From 78d0b4fd54b8b23c7481fff5051df8f03da42d94 Mon Sep 17 00:00:00 2001 From: StageGuard <45701251+StageGuard@users.noreply.github.com> Date: Sat, 18 Mar 2023 21:52:31 +0800 Subject: [PATCH] QRCode login support & Introduce new authorization factory (#2502) * [core] process `wtlogin.trans_emp` to support qrcode login * [core] fix `wtlogin.trans_emp` protocol * [core] optimize QRCodeLoginProcessor logic * [core] fix `wtlogin.trans_emp` outgoing packet * [core] cancel login when logging a bot which is inconsistent from bot factory * [core] ignore `flag3` check on ANDROID_WATCH & name `flag1` and `flag2` * [core] provide default `QRCodeLoginListener` for jvm * [core] don't catch IllegalStateException in QRCodeLoginProcessor * [core] Use `LoginSolver.createQRCodeLoginListener()` instead of property; Rename configuration name * [core] Code improvement * [core] remove qrcode state lock * [core] ignore `flag3` when command is `wtlogin.trans_emp` in packet codec * [core] enable qrcode login for macos * [core] remove debug property in log * [core] reformat code * [core] rename `TransEmpResponse` to `Response` * [core] assert `flag3Exception` not null first * [core] remove arg client * [core] update qrcode login notes * [core] set custom qrcode size * [core] Draft BotAuthorization * [core] make SecretsProtection mpp * [core] BotAuthorization.byXXX * [core] Move QRCodeLoginListener to `.auth` * [core] Protect data of BotAccount * [core] Add SelectorRequireReconnectException * [core] Implementation of BotAuthorization * Revert changes of BotConfiguration * api dump * [core] remove passwordMd5 in `BotAccount` * [mock] Add new bot factory function to mock bot factory * Delete LoginCommandTest * [core] Improve QRCode render * [core] Introduce UnsupportedCaptchaMethodException & UnsupportedQRCodeCaptchaException * api dump * update docs * [core] update `DebugRunHelper` * [core] add simple block for BotAuthorization * api dump * Rename `canDoQRLogin` to `supportsQRLogin`, and specify argument names for MiraiProtocolInternal * Remove `phoneNumber` parameter from BotAccount * Make `BotAccount.` with String password parameter TestOnly * Rename `InconsistentBotException` to `InconsistentBotIdException` * Rename `QRCodeLoginListener.onStatusChanged` to `QRCodeLoginListener.onStateChanged` * Rename `BotAuthorizationResult` to `BotAuthResult` * Rename BotAuthComponent, move internal APIs to internal module * Logic fixup * doc update * QRCodeLoginListener.qrCodeStateUpdateInterval & onIntervalLoop * console login with BotAuthorization * update testing * Update mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt * Move AuthControl outside SsoProcessor * Redesign auth * Add initialTicket to producerCoroutine * Revert protocol changes of MACOS * Fix latch death locking * Fix CoroutineOnDemandValueScope.receiveOrNull exceptional finish * Fix exception collecting * Fix DefaultBotAuthorizationFactory loading * [core] qrcode login for IPAD protocol * Revert "[core] qrcode login for IPAD protocol" This reverts commit c1136a8798917f9fbf96d304c16c02c57d3eb607. --------- Co-authored-by: Karlatemp Co-authored-by: Him188 --- .../compatibility-validation/jvm/api/jvm.api | 1 + .../backend/mirai-console/src/MiraiConsole.kt | 35 ++- .../MiraiConsoleImplementationBridge.kt | 8 + .../internal/auth/ConsoleBotAuthorization.kt | 53 ++++ .../internal/auth/ConsoleSecretsCalculator.kt | 53 ++++ .../test/command/LoginCommandTest.kt | 157 ----------- .../android/api/android.api | 78 +++++- .../compatibility-validation/jvm/api/jvm.api | 79 +++++- .../src/commonMain/kotlin/BotFactory.kt | 52 ++++ .../kotlin/auth/BotAuthorization.kt | 128 +++++++++ .../kotlin/auth/QRCodeLoginListener.kt | 95 +++++++ .../kotlin/network/LoginFailedException.kt | 40 ++- .../commonMain/kotlin/utils/LoginSolver.kt | 19 +- .../jvmMain/kotlin/utils/LoginSolver.jvm.kt | 158 ++++++++++- .../src/internal/MockBotFactoryImpl.kt | 8 + .../commonMain/kotlin/SecretsProtection.kt | 91 +++++++ .../jvmBaseMain/kotlin/SecretsProtection.kt | 75 ++---- .../jvmTest/kotlin/SecretsProtectionTest.kt | 4 +- .../nativeMain/kotlin/SecretsProtection.kt | 47 ++++ .../src/commonMain/kotlin/BotAccount.kt | 28 +- .../src/commonMain/kotlin/BotFactory.kt | 9 +- .../src/commonMain/kotlin/QQAndroidBot.kt | 4 + .../kotlin/network/QQAndroidClient.kt | 2 +- .../kotlin/network/auth/AuthControl.kt | 116 +++++++++ .../kotlin/network/auth/BotAuthSession.kt | 51 ++++ .../auth/CoroutineOnDemandValueScope.kt | 203 +++++++++++++++ .../DefaultBotAuthorizationFactoryImpl.kt | 51 ++++ .../commonMain/kotlin/network/auth/Latch.kt | 51 ++++ .../kotlin/network/auth/OnDemandValueScope.kt | 86 ++++++ .../kotlin/network/auth/ProducerState.kt | 176 +++++++++++++ .../components/AccountSecretsManager.kt | 4 +- .../network/components/BotClientHolder.kt | 6 + .../kotlin/network/components/PacketCodec.kt | 103 +++++--- .../components/QRCodeLoginProcessor.kt | 167 ++++++++++++ .../kotlin/network/components/SsoProcessor.kt | 198 +++++++++++--- .../network/handler/CommonNetworkHandler.kt | 9 +- .../SelectorRequireReconnectException.kt | 15 ++ .../src/commonMain/kotlin/network/keys.kt | 11 + .../network/protocol/packet/OutgoingPacket.kt | 6 +- .../network/protocol/packet/PacketFactory.kt | 1 + .../kotlin/network/protocol/packet/Tlv.kt | 96 ++++++- .../network/protocol/packet/login/WtLogin.kt | 246 +++++++++++++++++- .../packet/login/wtlogin/WtLogin15.kt | 4 +- .../protocol/packet/login/wtlogin/WtLogin9.kt | 57 +++- .../kotlin/utils/MiraiCoreServices.kt | 7 +- .../kotlin/utils/MiraiProtocolInternal.kt | 111 ++++---- ....mirai.auth.DefaultBotAuthorizationFactory | 10 + .../network/component/BotAuthControlTest.kt | 108 ++++++++ .../AbstractRealNetworkHandlerTest.kt | 30 ++- .../src/jvmBaseMain/kotlin/BotAccount.kt | 63 ----- .../SelectorRequireReconnectException.kt | 29 +++ .../kotlin/directboot/DebugRunHelper.kt | 7 +- .../src/nativeMain/kotlin/BotAccount.kt | 41 --- .../SelectorRequireReconnectException.kt | 12 + 54 files changed, 2817 insertions(+), 482 deletions(-) create mode 100644 mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt create mode 100644 mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt delete mode 100644 mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt create mode 100644 mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt create mode 100644 mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/auth/BotAuthSession.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/auth/CoroutineOnDemandValueScope.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/auth/DefaultBotAuthorizationFactoryImpl.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/auth/Latch.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/auth/OnDemandValueScope.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/components/QRCodeLoginProcessor.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt create mode 100644 mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.auth.DefaultBotAuthorizationFactory create mode 100644 mirai-core/src/commonTest/kotlin/network/component/BotAuthControlTest.kt delete mode 100644 mirai-core/src/jvmBaseMain/kotlin/BotAccount.kt create mode 100644 mirai-core/src/jvmBaseMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt delete mode 100644 mirai-core/src/nativeMain/kotlin/BotAccount.kt create mode 100644 mirai-core/src/nativeMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt diff --git a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api index 846472f7b..603748d78 100644 --- a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api +++ b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api @@ -19,6 +19,7 @@ public abstract interface class net/mamoe/mirai/console/MiraiConsole : kotlinx/c public final class net/mamoe/mirai/console/MiraiConsole$INSTANCE : net/mamoe/mirai/console/MiraiConsole { public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;JLjava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot; + public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;JLnet/mamoe/mirai/auth/BotAuthorization;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot; public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;J[BLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot; public fun getBuildDate ()Ljava/time/Instant; public fun getBuiltInPluginLoaders ()Ljava/util/List; diff --git a/mirai-console/backend/mirai-console/src/MiraiConsole.kt b/mirai-console/backend/mirai-console/src/MiraiConsole.kt index daa4abf0a..5dc6ab70d 100644 --- a/mirai-console/backend/mirai-console/src/MiraiConsole.kt +++ b/mirai-console/backend/mirai-console/src/MiraiConsole.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.* import me.him188.kotlin.dynamic.delegation.dynamicDelegation import net.mamoe.mirai.Bot import net.mamoe.mirai.BotFactory +import net.mamoe.mirai.auth.BotAuthorization import net.mamoe.mirai.console.MiraiConsole.INSTANCE import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.extensions.BotConfigurationAlterer @@ -191,8 +192,31 @@ public interface MiraiConsole : CoroutineScope { public fun addBot(id: Long, password: ByteArray, configuration: BotConfiguration.() -> Unit = {}): Bot = addBotImpl(id, password, configuration) + /** + * 添加一个 [Bot] 实例到全局 Bot 列表, 但不登录. + * + * 调用 [Bot.login] 可登录. + * + * @see Bot.instances 获取现有 [Bot] 实例列表 + * @see BotConfigurationAlterer ExtensionPoint + */ + @ConsoleExperimentalApi("This is a low-level API and might be removed in the future.") + public fun addBot( + id: Long, + authorization: BotAuthorization, + configuration: BotConfiguration.() -> Unit = {} + ): Bot = addBotImpl(id, authorization, configuration) + @Suppress("UNREACHABLE_CODE") - private fun addBotImpl(id: Long, password: Any, configuration: BotConfiguration.() -> Unit = {}): Bot { + private fun addBotImpl(id: Long, authorization: Any, configuration: BotConfiguration.() -> Unit = {}): Bot { + when (authorization) { + is String -> {} + is ByteArray -> {} + is BotAuthorization -> {} + + else -> throw IllegalArgumentException("Bad authorization type: `${authorization.javaClass.name}`. Require String, ByteArray or BotAuthorization") + } + var config = BotConfiguration().apply { workingDir = MiraiConsole.rootDir @@ -239,10 +263,11 @@ public interface MiraiConsole : CoroutineScope { extension.alterConfiguration(id, acc) } - return when (password) { - is ByteArray -> BotFactory.newBot(id, password, config) - is String -> BotFactory.newBot(id, password, config) - else -> throw IllegalArgumentException("Bad password type: `${password.javaClass.name}`. Require ByteArray or String") + return when (authorization) { + is ByteArray -> BotFactory.newBot(id, authorization, config) // pwd md5 + is String -> BotFactory.newBot(id, authorization, config) // pwd + is BotAuthorization -> BotFactory.newBot(id, authorization, config) // authorization + else -> error("assert") } } diff --git a/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt b/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt index 19e182d71..c43df6cb7 100644 --- a/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt +++ b/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt @@ -33,6 +33,7 @@ import net.mamoe.mirai.console.extensions.CommandCallParserProvider import net.mamoe.mirai.console.extensions.CommandCallResolverProvider import net.mamoe.mirai.console.extensions.PermissionServiceProvider import net.mamoe.mirai.console.extensions.PostStartupExtension +import net.mamoe.mirai.console.internal.auth.ConsoleSecretsCalculator import net.mamoe.mirai.console.internal.command.CommandConfig import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.ConfigurationKey @@ -100,6 +101,9 @@ internal class MiraiConsoleImplementationBridge( @Volatile var permissionSeviceLoaded: Boolean = false + // For protect account.secrets in console with non-password login + lateinit var consoleSecretsCalculator: ConsoleSecretsCalculator + // MiraiConsoleImplementation define: get() = LoggerControllerImpl() // Need to cache it or else created every call. // It caused config/Console/Logger.yml ignored. @@ -290,6 +294,10 @@ ___ ____ _ _____ _ phase("initialize all plugins") { pluginManager // init + consoleSecretsCalculator = ConsoleSecretsCalculator( + pluginManager.pluginsDataPath.resolve("Console/console-secrets.key") + ).also { it.consoleKey } + mainLogger.verbose { "Loading JVM plugins..." } pluginManager.loadAllPluginsUsingBuiltInLoaders() pluginManager.initExternalPluginLoaders().let { count -> diff --git a/mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt b/mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt new file mode 100644 index 000000000..b07a357f3 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt @@ -0,0 +1,53 @@ +/* + * 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.console.internal.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.console.MiraiConsoleImplementation +import java.io.ByteArrayOutputStream + +internal class ConsoleBotAuthorization( + private val delegate: suspend (BotAuthSession, BotAuthInfo) -> BotAuthResult, +) : BotAuthorization { + + override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult { + return delegate.invoke(session, info) + } + + override fun calculateSecretsKey(bot: BotAuthInfo): ByteArray { + val calc = MiraiConsoleImplementation.getBridge().consoleSecretsCalculator + + val writer = ByteArrayOutputStream() + + writer += calc.consoleKey.asByteArray + + writer += bot.deviceInfo.apn + writer += bot.deviceInfo.device + writer += bot.deviceInfo.bootId + writer += bot.deviceInfo.imsiMd5 + + return writer.toByteArray() + } + + + private operator fun ByteArrayOutputStream.plusAssign(data: ByteArray) { + write(data) + } + + companion object { + fun byQRCode(): ConsoleBotAuthorization = ConsoleBotAuthorization { session, _ -> + session.authByQRCode() + } + } +} + diff --git a/mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt b/mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt new file mode 100644 index 000000000..73f0395c9 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt @@ -0,0 +1,53 @@ +/* + * 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.console.internal.auth + +import net.mamoe.mirai.utils.SecretsProtection +import net.mamoe.mirai.utils.lateinitMutableProperty +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import java.nio.file.Path +import java.util.* +import kotlin.io.path.createDirectories +import kotlin.io.path.isRegularFile +import kotlin.io.path.readBytes +import kotlin.io.path.writeBytes + +internal class ConsoleSecretsCalculator( + private val file: Path, +) { + internal val consoleKey: SecretsProtection.EscapedByteBuffer get() = _consoleKey + + private var _consoleKey: SecretsProtection.EscapedByteBuffer by lateinitMutableProperty { + loadOrCreate() + } + + fun loadOrCreate(): SecretsProtection.EscapedByteBuffer { + if (file.isRegularFile()) { + return SecretsProtection.EscapedByteBuffer(file.readBytes()) + } + + file.parent?.createDirectories() + val dataStream = ByteArrayOutputStream() + val dataWriter = DataOutputStream(dataStream) + + repeat(3) { + dataWriter.writeUTF(UUID.randomUUID().toString()) + } + + val data = dataStream.toByteArray() + file.writeBytes(data) + return SecretsProtection.EscapedByteBuffer(data) + } + + fun reloadOrCreate() { + _consoleKey = loadOrCreate() + } +} \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt b/mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt deleted file mode 100644 index 9f839e281..000000000 --- a/mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2019-2022 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 - */ - -@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - -package net.mamoe.mirai.console.command - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.test.runTest -import net.mamoe.mirai.Bot -import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register -import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors -import net.mamoe.mirai.console.internal.command.builtin.LoginCommandImpl -import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig -import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account -import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.PasswordKind -import net.mamoe.mirai.internal.QQAndroidBot -import net.mamoe.mirai.utils.BotConfiguration -import net.mamoe.mirai.utils.md5 -import net.mamoe.mirai.utils.toUHexString -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -@OptIn(ExperimentalCommandDescriptors::class) -internal class LoginCommandTest : AbstractCommandTest() { - - @Test - fun `login with provided password`() = runTest { - val myId = 123L - val myPwd = "password001" - - val bot = awaitDeferred { cont -> - val command = object : LoginCommandImpl() { - override suspend fun doLogin(bot: Bot) { - cont.complete(bot as QQAndroidBot) - } - } - command.register(true) - command.execute(consoleSender, "$myId $myPwd") - } - - val account = bot.account - assertContentEquals(myPwd.md5(), account.passwordMd5) - assertEquals(myId, account.id) - } - - @Test - fun `login with saved plain password`() = runTest { - val myId = 123L - val myPwd = "password001" - - dataScope.set(AutoLoginConfig().apply { - accounts.add( - Account( - account = myId.toString(), - password = Account.Password(PasswordKind.PLAIN, myPwd) - ) - ) - }) - - val bot = awaitDeferred { cont -> - val command = object : LoginCommandImpl() { - override suspend fun doLogin(bot: Bot) { - cont.complete(bot as QQAndroidBot) - } - } - command.register(true) - command.execute(consoleSender, "$myId") - } - - val account = bot.account - assertContentEquals(myPwd.md5(), account.passwordMd5) - assertEquals(myId, account.id) - } - - @Test - fun `login with saved md5 password`() = runTest { - val myId = 123L - val myPwd = "password001" - - dataScope.set(AutoLoginConfig().apply { - accounts.add( - Account( - account = myId.toString(), - password = Account.Password(PasswordKind.MD5, myPwd.md5().toUHexString("")) - ) - ) - }) - - val bot = awaitDeferred { cont -> - val command = object : LoginCommandImpl() { - override suspend fun doLogin(bot: Bot) { - cont.complete(bot as QQAndroidBot) - } - } - command.register(true) - command.execute(consoleSender, "$myId") - } - - val account = bot.account - assertContentEquals(myPwd.md5(), account.passwordMd5) - assertEquals(myId, account.id) - } - - @Test - fun `login with saved configuration`() = runTest { - val myId = 123L - val myPwd = "password001" - - dataScope.set(AutoLoginConfig().apply { - accounts.add( - Account( - account = myId.toString(), - password = Account.Password(PasswordKind.MD5, myPwd.md5().toUHexString("")), - configuration = mapOf( - Account.ConfigurationKey.protocol to BotConfiguration.MiraiProtocol.ANDROID_PAD.name, - Account.ConfigurationKey.device to "device.new.json", - Account.ConfigurationKey.heartbeatStrategy to BotConfiguration.HeartbeatStrategy.REGISTER.name - ) - ) - ) - }) - - val bot = awaitDeferred { cont -> - val command = object : LoginCommandImpl() { - override suspend fun doLogin(bot: Bot) { - cont.complete(bot as QQAndroidBot) - } - } - command.register(true) - command.execute(consoleSender, "$myId") - } - - val configuration = bot.configuration - assertEquals(BotConfiguration.MiraiProtocol.ANDROID_PAD, configuration.protocol) - assertEquals(BotConfiguration.HeartbeatStrategy.REGISTER, configuration.heartbeatStrategy) - assertNotNull(configuration.deviceInfo) - } -} - -@BuilderInference -internal suspend inline fun awaitDeferred( - @BuilderInference - crossinline block: suspend (CompletableDeferred) -> Unit -): T { - val deferred = CompletableDeferred() - block(deferred) - return deferred.await() -} \ No newline at end of file diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api index 5da4649da..2d8d5bbd3 100644 --- a/mirai-core-api/compatibility-validation/android/api/android.api +++ b/mirai-core-api/compatibility-validation/android/api/android.api @@ -53,6 +53,9 @@ public abstract interface class net/mamoe/mirai/BotFactory { public fun newBot (JLjava/lang/String;)Lnet/mamoe/mirai/Bot; public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot; public abstract fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; + public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;)Lnet/mamoe/mirai/Bot; + public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot; + public abstract fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; public fun newBot (J[B)Lnet/mamoe/mirai/Bot; public fun newBot (J[BLnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot; public abstract fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; @@ -65,6 +68,7 @@ public abstract interface class net/mamoe/mirai/BotFactory$BotConfigurationLambd public final class net/mamoe/mirai/BotFactory$INSTANCE : net/mamoe/mirai/BotFactory { public final synthetic fun newBot (JLjava/lang/String;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot; public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; + public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; public final synthetic fun newBot (J[BLkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot; public fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; } @@ -165,6 +169,58 @@ public final class net/mamoe/mirai/_MiraiInstance { public static final fun set (Lnet/mamoe/mirai/IMirai;)V } +public abstract interface class net/mamoe/mirai/auth/BotAuthInfo { + public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration; + public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo; + public abstract fun getId ()J +} + +public abstract interface class net/mamoe/mirai/auth/BotAuthResult { +} + +public abstract interface class net/mamoe/mirai/auth/BotAuthSession { + public abstract fun authByPassword (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun authByPassword ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun authByQRCode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class net/mamoe/mirai/auth/BotAuthorization { + public static final field Companion Lnet/mamoe/mirai/auth/BotAuthorization$Companion; + public abstract fun authorize (Lnet/mamoe/mirai/auth/BotAuthSession;Lnet/mamoe/mirai/auth/BotAuthInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization; + public static fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization; + public static fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization; + public fun calculateSecretsKey (Lnet/mamoe/mirai/auth/BotAuthInfo;)[B +} + +public final class net/mamoe/mirai/auth/BotAuthorization$Companion { + public final fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization; + public final fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization; + public final fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization; + public final fun invoke (Lkotlin/jvm/functions/Function3;)Lnet/mamoe/mirai/auth/BotAuthorization; +} + +public abstract interface class net/mamoe/mirai/auth/QRCodeLoginListener { + public fun getQrCodeEcLevel ()I + public fun getQrCodeMargin ()I + public fun getQrCodeSize ()I + public fun getQrCodeStateUpdateInterval ()J + public abstract fun onFetchQRCode (Lnet/mamoe/mirai/Bot;[B)V + public fun onIntervalLoop ()V + public abstract fun onStateChanged (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;)V +} + +public final class net/mamoe/mirai/auth/QRCodeLoginListener$State : java/lang/Enum { + public static final field CANCELLED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field CONFIRMED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field DEFAULT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field TIMEOUT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field WAITING_FOR_CONFIRM Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field WAITING_FOR_SCAN Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static fun values ()[Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; +} + public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/mamoe/mirai/contact/Member { public abstract fun getAnonymousId ()Ljava/lang/String; public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge; @@ -5355,6 +5411,12 @@ public final class net/mamoe/mirai/network/ForceOfflineException : java/util/con public fun getMessage ()Ljava/lang/String; } +public final class net/mamoe/mirai/network/InconsistentBotIdException : net/mamoe/mirai/network/LoginFailedException { + public synthetic fun (JJLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getActual ()J + public final fun getExpected ()J +} + public abstract class net/mamoe/mirai/network/LoginFailedException : java/lang/RuntimeException { public synthetic fun (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (ZLjava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -5374,11 +5436,22 @@ public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException { +public class net/mamoe/mirai/network/UnsupportedCaptchaMethodException : net/mamoe/mirai/network/LoginFailedException { + public fun (Z)V + public fun (ZLjava/lang/String;)V + public fun (ZLjava/lang/String;Ljava/lang/Throwable;)V + public fun (ZLjava/lang/Throwable;)V +} + +public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException { public fun (Ljava/lang/String;)V } -public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException { +public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException { + public fun (Ljava/lang/String;)V +} + +public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException { public fun (Ljava/lang/String;)V } @@ -5825,6 +5898,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver { public static final field Companion Lnet/mamoe/mirai/utils/LoginSolver$Companion; public static final field Default Lnet/mamoe/mirai/utils/LoginSolver; public fun ()V + public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener; public fun isSliderCaptchaSupported ()Z public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api index 98c58ede2..a19cb5a71 100644 --- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api +++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api @@ -53,6 +53,9 @@ public abstract interface class net/mamoe/mirai/BotFactory { public fun newBot (JLjava/lang/String;)Lnet/mamoe/mirai/Bot; public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot; public abstract fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; + public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;)Lnet/mamoe/mirai/Bot; + public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot; + public abstract fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; public fun newBot (J[B)Lnet/mamoe/mirai/Bot; public fun newBot (J[BLnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot; public abstract fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; @@ -65,6 +68,7 @@ public abstract interface class net/mamoe/mirai/BotFactory$BotConfigurationLambd public final class net/mamoe/mirai/BotFactory$INSTANCE : net/mamoe/mirai/BotFactory { public final synthetic fun newBot (JLjava/lang/String;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot; public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; + public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; public final synthetic fun newBot (J[BLkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot; public fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot; } @@ -165,6 +169,58 @@ public final class net/mamoe/mirai/_MiraiInstance { public static final fun set (Lnet/mamoe/mirai/IMirai;)V } +public abstract interface class net/mamoe/mirai/auth/BotAuthInfo { + public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration; + public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo; + public abstract fun getId ()J +} + +public abstract interface class net/mamoe/mirai/auth/BotAuthResult { +} + +public abstract interface class net/mamoe/mirai/auth/BotAuthSession { + public abstract fun authByPassword (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun authByPassword ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun authByQRCode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class net/mamoe/mirai/auth/BotAuthorization { + public static final field Companion Lnet/mamoe/mirai/auth/BotAuthorization$Companion; + public abstract fun authorize (Lnet/mamoe/mirai/auth/BotAuthSession;Lnet/mamoe/mirai/auth/BotAuthInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization; + public static fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization; + public static fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization; + public fun calculateSecretsKey (Lnet/mamoe/mirai/auth/BotAuthInfo;)[B +} + +public final class net/mamoe/mirai/auth/BotAuthorization$Companion { + public final fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization; + public final fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization; + public final fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization; + public final fun invoke (Lkotlin/jvm/functions/Function3;)Lnet/mamoe/mirai/auth/BotAuthorization; +} + +public abstract interface class net/mamoe/mirai/auth/QRCodeLoginListener { + public fun getQrCodeEcLevel ()I + public fun getQrCodeMargin ()I + public fun getQrCodeSize ()I + public fun getQrCodeStateUpdateInterval ()J + public abstract fun onFetchQRCode (Lnet/mamoe/mirai/Bot;[B)V + public fun onIntervalLoop ()V + public abstract fun onStateChanged (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;)V +} + +public final class net/mamoe/mirai/auth/QRCodeLoginListener$State : java/lang/Enum { + public static final field CANCELLED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field CONFIRMED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field DEFAULT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field TIMEOUT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field WAITING_FOR_CONFIRM Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static final field WAITING_FOR_SCAN Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; + public static fun values ()[Lnet/mamoe/mirai/auth/QRCodeLoginListener$State; +} + public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/mamoe/mirai/contact/Member { public abstract fun getAnonymousId ()Ljava/lang/String; public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge; @@ -5355,6 +5411,12 @@ public final class net/mamoe/mirai/network/ForceOfflineException : java/util/con public fun getMessage ()Ljava/lang/String; } +public final class net/mamoe/mirai/network/InconsistentBotIdException : net/mamoe/mirai/network/LoginFailedException { + public synthetic fun (JJLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getActual ()J + public final fun getExpected ()J +} + public abstract class net/mamoe/mirai/network/LoginFailedException : java/lang/RuntimeException { public synthetic fun (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (ZLjava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -5374,11 +5436,22 @@ public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException { +public class net/mamoe/mirai/network/UnsupportedCaptchaMethodException : net/mamoe/mirai/network/LoginFailedException { + public fun (Z)V + public fun (ZLjava/lang/String;)V + public fun (ZLjava/lang/String;Ljava/lang/Throwable;)V + public fun (ZLjava/lang/Throwable;)V +} + +public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException { public fun (Ljava/lang/String;)V } -public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException { +public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException { + public fun (Ljava/lang/String;)V +} + +public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException { public fun (Ljava/lang/String;)V } @@ -5825,6 +5898,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver { public static final field Companion Lnet/mamoe/mirai/utils/LoginSolver$Companion; public static final field Default Lnet/mamoe/mirai/utils/LoginSolver; public fun ()V + public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener; public fun isSliderCaptchaSupported ()Z public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -6190,6 +6264,7 @@ public final class net/mamoe/mirai/utils/StandardCharImageLoginSolver : net/mamo public synthetic fun (Lkotlin/jvm/functions/Function1;Lnet/mamoe/mirai/utils/MiraiLogger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun createBlocking (Lkotlin/jvm/functions/Function0;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver; public static final fun createBlocking (Lkotlin/jvm/functions/Function0;Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver; + public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener; public fun isSliderCaptchaSupported ()Z public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/mirai-core-api/src/commonMain/kotlin/BotFactory.kt b/mirai-core-api/src/commonMain/kotlin/BotFactory.kt index eaa877471..e8bf56a2c 100644 --- a/mirai-core-api/src/commonMain/kotlin/BotFactory.kt +++ b/mirai-core-api/src/commonMain/kotlin/BotFactory.kt @@ -11,6 +11,7 @@ package net.mamoe.mirai +import net.mamoe.mirai.auth.BotAuthorization import net.mamoe.mirai.utils.BotConfiguration import kotlin.jvm.JvmSynthetic @@ -111,6 +112,53 @@ public interface BotFactory { */ public fun newBot(qq: Long, passwordMd5: ByteArray): Bot = newBot(qq, passwordMd5, BotConfiguration.Default) + /////////////////////////////////////////////////////////////////////////// + // BotAuthorization + /////////////////////////////////////////////////////////////////////////// + + /** + * 使用 [默认配置][BotConfiguration.Default] 构造 [Bot] 实例 + * + * @since 2.15 + */ + public fun newBot(qq: Long, authorization: BotAuthorization): Bot = + newBot(qq, authorization, BotConfiguration.Default) + + /** + * 使用指定的 [配置][configuration] 构造 [Bot] 实例 + * + * @since 2.15 + */ + public fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot + + + /** + * 使用指定的 [配置][configuration] 构造 [Bot] 实例 + * + * Kotlin: + * ``` + * newBot(123, password) { + * // this: BotConfiguration + * fileBasedDeviceInfo() + * } + * ``` + * + * Java: + * ```java + * newBot(123, password, configuration -> { + * configuration.fileBasedDeviceInfo() + * }) + * ``` + * + * @since 2.15 + */ + public fun newBot( + qq: Long, + authorization: BotAuthorization, + configuration: BotConfigurationLambda /* = BotConfiguration.() -> Unit */ + ): Bot = newBot(qq, authorization, configuration.run { BotConfiguration().apply { invoke() } }) + + public companion object INSTANCE : BotFactory { override fun newBot(qq: Long, password: String, configuration: BotConfiguration): Bot { return Mirai.BotFactory.newBot(qq, password, configuration) @@ -160,5 +208,9 @@ public interface BotFactory { passwordMd5: ByteArray, configuration: BotConfiguration.() -> Unit /* = BotConfiguration.() -> Unit */ ): Bot = newBot(qq, passwordMd5, BotConfiguration().apply(configuration)) + + override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot { + return Mirai.BotFactory.newBot(qq, authorization, configuration) + } } } \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt b/mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt new file mode 100644 index 000000000..03d2db9a3 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt @@ -0,0 +1,128 @@ +/* + * 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.auth + +import net.mamoe.mirai.BotFactory +import net.mamoe.mirai.Mirai +import net.mamoe.mirai.network.LoginFailedException +import net.mamoe.mirai.network.RetryLaterException +import net.mamoe.mirai.utils.* +import kotlin.jvm.JvmStatic + +/** + * Bot 的登录鉴权方式 + * + * @see BotFactory.newBot + * + * @since 2.15 + */ +public interface BotAuthorization { + /** + * 此方法控制 Bot 如何进行登录. + * + * Bot 只能使用一种登录方式, 但是可以在一种登录方式失败的时候尝试其他登录方式 + * + * ## 异常类型 + * + * 抛出一个 [LoginFailedException] 以正常地终止登录, 并可建议系统进行重连或停止 bot (通过 [LoginFailedException.killBot]). + * 例如抛出 [RetryLaterException] 可让 bot 重新进行一次登录. + * + * 抛出任意其他 [Throwable] 将视为鉴权选择器的自身错误. + * + * ## 示例代码 + * ```kotlin + * override suspend fun authorize( + * authComponent: BotAuthSession, + * bot: BotAuthInfo, + * ) { + * return kotlin.runCatching { + * authComponent.authByQRCode() + * }.recover { + * authComponent.authByPassword("...") + * }.getOrThrow() + * } + * ``` + */ + public suspend fun authorize( + session: BotAuthSession, + info: BotAuthInfo, + ): BotAuthResult + + + /** + * 计算 `cache/account.secrets` 的加密秘钥 + */ + public fun calculateSecretsKey( + bot: BotAuthInfo, + ): ByteArray = bot.deviceInfo.guid + bot.id.toByteArray() + + public companion object { + @JvmStatic + public fun byPassword(password: String): BotAuthorization = byPassword(password.md5()) + + @JvmStatic + public fun byPassword(passwordMd5: ByteArray): BotAuthorization = factory.byPassword(passwordMd5) + + @JvmStatic + public fun byQRCode(): BotAuthorization = factory.byQRCode() + + public operator fun invoke( + block: suspend (BotAuthSession, BotAuthInfo) -> BotAuthResult + ): BotAuthorization { + return object : BotAuthorization { + override suspend fun authorize( + session: BotAuthSession, + info: BotAuthInfo + ): BotAuthResult { + return block(session, info) + } + } + } + + private val factory: DefaultBotAuthorizationFactory by lazy { + Mirai // Ensure services loaded + loadService() + } + } +} + +@NotStableForInheritance +public interface BotAuthResult + +@NotStableForInheritance +public interface BotAuthInfo { + public val id: Long + public val deviceInfo: DeviceInfo + public val configuration: BotConfiguration +} + +@NotStableForInheritance +public interface BotAuthSession { + /** + * @throws LoginFailedException + */ + public suspend fun authByPassword(password: String): BotAuthResult + + /** + * @throws LoginFailedException + */ + public suspend fun authByPassword(passwordMd5: ByteArray): BotAuthResult + + /** + * @throws LoginFailedException + */ + public suspend fun authByQRCode(): BotAuthResult +} + + +internal interface DefaultBotAuthorizationFactory { + fun byPassword(passwordMd5: ByteArray): BotAuthorization + fun byQRCode(): BotAuthorization +} diff --git a/mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt b/mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt new file mode 100644 index 000000000..1a2245417 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt @@ -0,0 +1,95 @@ +/* + * 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.auth + +import net.mamoe.mirai.Bot +import net.mamoe.mirai.network.LoginFailedException + +/** + * 二维码扫描登录监听器 + * + * @since 2.15 + */ +public interface QRCodeLoginListener { + + /** + * 使用二维码登录时获取的二维码图片大小字节数. + */ + public val qrCodeSize: Int get() = 3 + + /** + * 使用二维码登录时获取的二维码边框宽度像素. + */ + public val qrCodeMargin: Int get() = 4 + + /** + * 使用二维码登录时获取的二维码校正等级,必须为 1-3 之间. + */ + public val qrCodeEcLevel: Int get() = 2 + + /** + * 每隔 [qrCodeStateUpdateInterval] 毫秒更新一次[二维码状态][State] + */ + public val qrCodeStateUpdateInterval: Long get() = 5000 + + /** + * 从服务器获取二维码时调用,在下级显示二维码并扫描. + * + * @param data 二维码图像数据 (文件) + */ + public fun onFetchQRCode(bot: Bot, data: ByteArray) + + /** + * 当二维码状态变化时调用. + * @see State + */ + public fun onStateChanged(bot: Bot, state: State) + + /** + * 每隔一段时间会调用一次此函数 + * + * 在此函数抛出 [LoginFailedException] 以中断登录 + */ + public fun onIntervalLoop() { + } + + public enum class State { + /** + * 等待扫描中,请在此阶段请扫描二维码. + * @see QRCodeLoginListener.onFetchQRCode + */ + WAITING_FOR_SCAN, + + /** + * 二维码已扫描,等待扫描端确认登录. + */ + WAITING_FOR_CONFIRM, + + /** + * 扫描后取消了确认. + */ + CANCELLED, + + /** + * 二维码超时,必须重新获取二维码. + */ + TIMEOUT, + + /** + * 二维码已确认,将会继续登录. + */ + CONFIRMED, + + /** + * 默认状态,在登录前通常为此状态. + */ + DEFAULT, + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt b/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt index 60b090c03..a8107c562 100644 --- a/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt +++ b/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -37,6 +37,21 @@ public class WrongPasswordException @MiraiInternalApi constructor( message: String? ) : LoginFailedException(true, message) +/** + * 二维码扫码账号与 BOT 账号不一致。 + * + * @since 2.15 + */ +public class InconsistentBotIdException @MiraiInternalApi constructor( + public val expected: Long, + public val actual: Long, + message: String? = null +) : LoginFailedException( + true, + message + ?: "trying to logging in a bot whose id is different from the one provided to BotFactory.newBot, expected=$expected, actual=$actual." +) + /** * 无可用服务器 */ @@ -60,16 +75,35 @@ public class NoStandardInputForCaptchaException @MiraiInternalApi constructor( public override val cause: Throwable? = null ) : LoginFailedException(true, "no standard input for captcha") +/** + * 当前 [LoginSolver] 不支持此验证方式 + * + * @since 2.15 + */ +public open class UnsupportedCaptchaMethodException : LoginFailedException { + public constructor(killBot: Boolean) : super(killBot) + public constructor(killBot: Boolean, message: String?) : super(killBot, message) + public constructor(killBot: Boolean, message: String?, cause: Throwable?) : super(killBot, message, cause) + public constructor(killBot: Boolean, cause: Throwable?) : super(killBot, cause = cause) +} + /** * 需要强制短信验证, 且当前 [LoginSolver] 不支持时抛出. * @since 2.13 */ -public class UnsupportedSmsLoginException(message: String?) : LoginFailedException(true, message) +public class UnsupportedSmsLoginException(message: String?) : UnsupportedCaptchaMethodException(true, message) /** * 无法完成滑块验证 */ -public class UnsupportedSliderCaptchaException(message: String?) : LoginFailedException(true, message) +public class UnsupportedSliderCaptchaException(message: String?) : UnsupportedCaptchaMethodException(true, message) + +/** + * 需要二维码登录, 且当前 [LoginSolver] 不支持时抛出 + * + * @since 2.15 + */ +public class UnsupportedQRCodeCaptchaException(message: String?) : UnsupportedCaptchaMethodException(true, message) /** * 非 mirai 实现的异常 diff --git a/mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt b/mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt index ce3371b91..0a59f62ac 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -13,8 +13,12 @@ package net.mamoe.mirai.utils import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.Bot +import net.mamoe.mirai.auth.BotAuthSession +import net.mamoe.mirai.auth.BotAuthorization +import net.mamoe.mirai.auth.QRCodeLoginListener import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.RetryLaterException +import net.mamoe.mirai.network.UnsupportedQRCodeCaptchaException import net.mamoe.mirai.network.UnsupportedSmsLoginException import net.mamoe.mirai.utils.LoginSolver.Companion.Default import kotlin.jvm.JvmField @@ -49,6 +53,19 @@ public abstract class LoginSolver { */ public open val isSliderCaptchaSupported: Boolean get() = PlatformLoginSolverImplementations.isSliderCaptchaSupported + /** + * 当使用二维码登录时会通过此方法创建二维码登录监听器 + * + * @see QRCodeLoginListener + * @see BotAuthorization + * @see BotAuthSession.authByQRCode + * + * @since 2.15 + */ + public open fun createQRCodeLoginListener(bot: Bot): QRCodeLoginListener { + throw UnsupportedQRCodeCaptchaException("This login session requires QRCode login, but current LoginSolver($this) does not support it. Please override `LoginSolver.createQRCodeLoginListener`.") + } + /** * 处理滑动验证码. * diff --git a/mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt b/mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt index b57c21c34..346457cc8 100644 --- a/mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt +++ b/mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -17,6 +17,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import net.mamoe.mirai.Bot +import net.mamoe.mirai.auth.QRCodeLoginListener import net.mamoe.mirai.network.NoStandardInputForCaptchaException import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking import java.awt.Image @@ -57,6 +58,105 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor( } override val isSliderCaptchaSupported: Boolean get() = true + override fun createQRCodeLoginListener(bot: Bot): QRCodeLoginListener { + return object : QRCodeLoginListener { + private var tmpFile: File? = null + + override val qrCodeMargin: Int get() = 1 + override val qrCodeSize: Int get() = 1 + + override fun onFetchQRCode(bot: Bot, data: ByteArray) { + val logger = loggerSupplier(bot) + + logger.info { "[QRCodeLogin] 已获取登录二维码,请在手机 QQ 使用账号 ${bot.id} 扫码" } + logger.info { "[QRCodeLogin] Fetched login qrcode, please scan via qq android with account ${bot.id}." } + + try { + val tempFile: File + if (tmpFile == null) { + tempFile = File.createTempFile( + "mirai-qrcode-${bot.id}-${currentTimeSeconds()}", + ".png" + ).apply { deleteOnExit() } + + tempFile.createNewFile() + + tmpFile = tempFile + } else { + tempFile = tmpFile!! + } + + tempFile.writeBytes(data) + logger.info { "[QRCodeLogin] 将会显示二维码图片,若看不清图片,请查看文件 ${tempFile.absolutePath}" } + logger.info { "[QRCodeLogin] Displaying qrcode image. If not clear, view file ${tempFile.absolutePath}." } + } catch (e: Exception) { + logger.warning("[QRCodeLogin] 无法写出二维码图片. 请尽量关闭终端个性化样式后扫描二维码字符图片", e) + logger.warning( + "[QRCodeLogin] Failed to export qrcode image. Please try to scan the char-image after disabling custom terminal style.", + e + ) + } + + data.inputStream().use { stream -> + try { + val isCacheEnabled = ImageIO.getUseCache() + + try { + ImageIO.setUseCache(false) + val img = ImageIO.read(stream) + if (img == null) { + logger.warning { "[QRCodeLogin] 无法创建字符图片. 请查看文件" } + logger.warning { "[QRCodeLogin] Failed to create char-image. Please see the file." } + } else { + logger.info { "[QRCodeLogin] \n" + img.renderQRCode() } + } + } finally { + ImageIO.setUseCache(isCacheEnabled) + } + + } catch (throwable: Throwable) { + logger.warning("[QRCodeLogin] 创建字符图片时出错. 请查看文件.", throwable) + logger.warning("[QRCodeLogin] Failed to create char-image. Please see the file.", throwable) + } + } + } + + override fun onStateChanged(bot: Bot, state: QRCodeLoginListener.State) { + val logger = loggerSupplier(bot) + logger.info { + buildString { + append("[QRCodeLogin] ") + when (state) { + QRCodeLoginListener.State.WAITING_FOR_SCAN -> append("等待扫描二维码中") + QRCodeLoginListener.State.WAITING_FOR_CONFIRM -> append("扫描完成,请在手机 QQ 确认登录") + QRCodeLoginListener.State.CANCELLED -> append("已取消登录,将会重新获取二维码") + QRCodeLoginListener.State.TIMEOUT -> append("扫描超时,将会重新获取二维码") + QRCodeLoginListener.State.CONFIRMED -> append("已确认登录") + else -> append("default state") + } + } + } + logger.info { + buildString { + append("[QRCodeLogin] ") + when (state) { + QRCodeLoginListener.State.WAITING_FOR_SCAN -> append("Waiting for scanning qrcode.") + QRCodeLoginListener.State.WAITING_FOR_CONFIRM -> append("Scan complete. Please confirm login.") + QRCodeLoginListener.State.CANCELLED -> append("Login cancelled, we will try to fetch qrcode again.") + QRCodeLoginListener.State.TIMEOUT -> append("Timeout scanning, we will try to fetch qrcode again.") + QRCodeLoginListener.State.CONFIRMED -> append("Login confirmed.") + else -> append("default state") + } + } + } + + if (state == QRCodeLoginListener.State.CONFIRMED) { + kotlin.runCatching { tmpFile?.delete() }.onFailure { logger.warning(it) } + } + } + + } + } override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock { val logger = loggerSupplier(bot) @@ -68,7 +168,7 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor( try { tempFile.writeBytes(data) logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" } - logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" } + logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}." } } catch (e: Exception) { logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e) logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e) @@ -282,3 +382,57 @@ private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Doub } } } + +private fun BufferedImage.renderQRCode( + blackPlaceholder: String = " ", + whitePlaceholder: String = " ", + doColorSwitch: Boolean = true, +): String { + var lastStatus: Boolean? = null + + fun isBlackBlock(rgb: Int): Boolean { + val r = rgb and 0xff0000 shr 16 + val g = rgb and 0x00ff00 shr 8 + val b = rgb and 0x0000ff + + return r < 10 && g < 10 && b < 10 + } + + val sb = StringBuilder() + sb.append("\n") + + val BLACK = "\u001b[30;40m" + val WHITE = "\u001b[97;107m" + val RESET = "\u001b[0m" + + for (y in 0 until height) { + for (x in 0 until width) { + val rgbcolor = getRGB(x, y) + val crtStatus = isBlackBlock(rgbcolor) + + if (doColorSwitch && crtStatus != lastStatus) { + lastStatus = crtStatus + sb.append( + if (crtStatus) BLACK else WHITE + ) + } + + sb.append( + if (crtStatus) blackPlaceholder else whitePlaceholder + ) + } + + if (doColorSwitch) { + sb.append(RESET) + } + + sb.append("\n") + lastStatus = null + } + + if (doColorSwitch) { + sb.append(RESET) + } + + return sb.toString() +} diff --git a/mirai-core-mock/src/internal/MockBotFactoryImpl.kt b/mirai-core-mock/src/internal/MockBotFactoryImpl.kt index 26a76517d..f6b2e6ed7 100644 --- a/mirai-core-mock/src/internal/MockBotFactoryImpl.kt +++ b/mirai-core-mock/src/internal/MockBotFactoryImpl.kt @@ -10,6 +10,7 @@ package net.mamoe.mirai.mock.internal import net.mamoe.mirai.Bot +import net.mamoe.mirai.auth.BotAuthorization import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.mock.MockBot import net.mamoe.mirai.mock.MockBotFactory @@ -115,4 +116,11 @@ internal class MockBotFactoryImpl : MockBotFactory { .configuration(configuration) .create() } + + override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot { + return newMockBotBuilder() + .id(qq) + .configuration(configuration) + .create() + } } \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt b/mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt new file mode 100644 index 000000000..4f83a598c --- /dev/null +++ b/mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt @@ -0,0 +1,91 @@ +/* + * 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.utils + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline +import kotlin.jvm.JvmStatic + +/** + * 核心数据保护器 + * + * ### Why + * + * 有时候可能会发生 `OutOfMemoryError`, 如果存在 `-XX:+HeapDumpOnOutOfMemoryError`, 则 JVM 会生成一份系统内存打印以供 debug. + * 该报告包含全部内存信息, 包括各种数据, 核心数据以及, 机密数据 (如密码) + * + * 该内存报告唯一没有包含的数据就是 Native层数据, 包括且不限于 + * + * - `sun.misc.Unsafe.allocate()` + * - `java.nio.ByteBuffer.allocateDirect()` (Named `DirectByteBuffer`) + * - C/C++ (或其他语言) 的数据 + * + * *试验数据来源 `openjdk version "17" 2021-09-14, 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)`* + * + * ### How it works + * + * 因为 Heap Dump 不存在 `DirectByteBuffer` 的实际数据, 所以可以通过该类隐藏关键数据. 等需要的时候在读取出来. + * 因为数据并没有直接存在于某个类字段中, 缺少数据关联, 也很难分析相关数据是什么数据 + */ +@Suppress("NOTHING_TO_INLINE", "UsePropertyAccessSyntax") +//@MiraiExperimentalApi +public object SecretsProtection { + + @JvmInline + @Serializable(EscapedStringSerializer::class) + public value class EscapedString( + public val data: Any, + ) { + public val asString: String + get() = SecretsProtectionPlatform.impl_asString(data) + + public constructor(data: ByteArray) : this(escape(data)) + public constructor(data: String) : this(escape(data.encodeToByteArray())) + } + + @JvmInline + @Serializable(EscapedByteBufferSerializer::class) + public value class EscapedByteBuffer( + public val data: Any, + ) { + public val size: Int get() = SecretsProtectionPlatform.impl_getSize(data) + + public val asByteArray: ByteArray + get() = SecretsProtectionPlatform.impl_asByteArray(data) + + public constructor(data: ByteArray) : this(escape(data)) + } + + @JvmStatic + public fun escape(data: ByteArray): Any { + return SecretsProtectionPlatform.escape(data) + } + + + public object EscapedStringSerializer : + KSerializer by SecretsProtectionPlatform.EscapedStringSerializer + + public object EscapedByteBufferSerializer : + KSerializer by SecretsProtectionPlatform.EscapedByteBufferSerializer +} + + +internal expect object SecretsProtectionPlatform { + fun impl_asString(data: Any): String + fun impl_asByteArray(data: Any): ByteArray + fun impl_getSize(data: Any): Int + + fun escape(data: ByteArray): Any + + object EscapedStringSerializer : KSerializer + + object EscapedByteBufferSerializer : KSerializer +} \ No newline at end of file diff --git a/mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt b/mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt index febce1f25..91d346a97 100644 --- a/mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt +++ b/mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt @@ -10,39 +10,15 @@ package net.mamoe.mirai.utils import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.builtins.serializer import java.nio.ByteBuffer -import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.atomic.AtomicIntegerFieldUpdater import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock -/** - * 核心数据保护器 - * - * ### Why - * - * 有时候可能会发生 `OutOfMemoryError`, 如果存在 `-XX:+HeapDumpOnOutOfMemoryError`, 则 JVM 会生成一份系统内存打印以供 debug. - * 该报告包含全部内存信息, 包括各种数据, 核心数据以及, 机密数据 (如密码) - * - * 该内存报告唯一没有包含的数据就是 Native层数据, 包括且不限于 - * - * - `sun.misc.Unsafe.allocate()` - * - `java.nio.ByteBuffer.allocateDirect()` (Named `DirectByteBuffer`) - * - C/C++ (或其他语言) 的数据 - * - * *试验数据来源 `openjdk version "17" 2021-09-14, 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)`* - * - * ### How it works - * - * 因为 Heap Dump 不存在 `DirectByteBuffer` 的实际数据, 所以可以通过该类隐藏关键数据. 等需要的时候在读取出来. - * 因为数据并没有直接存在于某个类字段中, 缺少数据关联, 也很难分析相关数据是什么数据 - */ -@Suppress("NOTHING_TO_INLINE", "UsePropertyAccessSyntax") -//@MiraiExperimentalApi -public object SecretsProtection { +internal actual object SecretsProtectionPlatform { + private class NativeBufferWithLock( @JvmField val buffer: ByteBuffer, val lock: Lock = ReentrantLock(), @@ -106,7 +82,7 @@ public object SecretsProtection { */ @JvmStatic - public fun allocate(size: Int): ByteBuffer { + fun allocate(size: Int): ByteBuffer { if (size >= bufferSize) { return ByteBuffer.allocateDirect(size) } @@ -171,39 +147,40 @@ public object SecretsProtection { } @JvmStatic - public fun escape(data: ByteArray): ByteBuffer { + actual fun escape(data: ByteArray): Any { return allocate(data.size).also { it.put(data) it.pos = 0 } } - @JvmInline - @Serializable(EscapedStringSerializer::class) - public value class EscapedString( - public val data: ByteBuffer, - ) { - public val asString: String - get() = data.duplicate().readString() + actual fun impl_asString(data: Any): String { + data as ByteBuffer + + return data.duplicate().readString() } - @JvmInline - @Serializable(EscapedByteBufferSerializer::class) - public value class EscapedByteBuffer( - public val data: ByteBuffer, - ) + actual fun impl_asByteArray(data: Any): ByteArray { + data as ByteBuffer + return data.duplicate().readBytes() + } - public object EscapedStringSerializer : KSerializer by String.serializer().map( + actual fun impl_getSize(data: Any): Int { + return (data as ByteBuffer).remaining + } + + actual object EscapedStringSerializer : KSerializer by String.serializer().map( String.serializer().descriptor.copy("EscapedString"), - deserialize = { EscapedString(escape(it.toByteArray())) }, - serialize = { it.data.duplicate().readString() } + deserialize = { SecretsProtection.EscapedString(escape(it.toByteArray())) }, + serialize = { it.data.cast().duplicate().readString() } ) - public object EscapedByteBufferSerializer : KSerializer by ByteArraySerializer().map( - ByteArraySerializer().descriptor.copy("EscapedByteBuffer"), - deserialize = { EscapedByteBuffer(escape(it)) }, - serialize = { it.data.duplicate().readBytes() } - ) + actual object EscapedByteBufferSerializer : + KSerializer by ByteArraySerializer().map( + ByteArraySerializer().descriptor.copy("EscapedByteBuffer"), + deserialize = { SecretsProtection.EscapedByteBuffer(escape(it)) }, + serialize = { it.data.cast().duplicate().readBytes() } + ) -} +} \ No newline at end of file diff --git a/mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt b/mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt index 750f30021..448b76bbf 100644 --- a/mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt +++ b/mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt @@ -12,9 +12,9 @@ package net.mamoe.mirai.utils import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.nio.ByteBuffer import kotlin.test.Test import kotlin.test.assertContentEquals -import kotlin.test.assertTrue internal class SecretsProtectionTest { @Test @@ -22,7 +22,7 @@ internal class SecretsProtectionTest { repeat(500) { launch { val data = ByteArray((1..255).random()) { (0..255).random().toByte() } - val buffer = SecretsProtection.escape(data) + val buffer = SecretsProtection.escape(data) as ByteBuffer assertContentEquals( data, buffer.duplicate().readBytes() ) diff --git a/mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt b/mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt new file mode 100644 index 000000000..a83b31818 --- /dev/null +++ b/mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.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.utils + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ByteArraySerializer +import kotlinx.serialization.builtins.serializer + +internal actual object SecretsProtectionPlatform { + actual fun impl_asString(data: Any): String { + return (data as ByteArray).decodeToString() + } + + actual fun impl_asByteArray(data: Any): ByteArray { + return data as ByteArray + } + + actual fun impl_getSize(data: Any): Int { + return data.cast().size + } + + actual fun escape(data: ByteArray): Any { + return data + } + + actual object EscapedStringSerializer : KSerializer by String.serializer().map( + String.serializer().descriptor.copy("EscapedString"), + deserialize = { SecretsProtection.EscapedString(it.encodeToByteArray()) }, + serialize = { it.data.cast().decodeToString() } + ) + + actual object EscapedByteBufferSerializer : + KSerializer by ByteArraySerializer().map( + ByteArraySerializer().descriptor.copy("EscapedByteBuffer"), + deserialize = { SecretsProtection.EscapedByteBuffer(it) }, + serialize = { it.data.cast() } + ) + + +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/BotAccount.kt b/mirai-core/src/commonMain/kotlin/BotAccount.kt index 611acb7ad..c05e1e959 100644 --- a/mirai-core/src/commonMain/kotlin/BotAccount.kt +++ b/mirai-core/src/commonMain/kotlin/BotAccount.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -10,16 +10,26 @@ package net.mamoe.mirai.internal +import net.mamoe.mirai.auth.BotAuthorization +import net.mamoe.mirai.utils.SecretsProtection +import net.mamoe.mirai.utils.TestOnly -internal expect class BotAccount { - internal val id: Long - val phoneNumber: String - constructor(id: Long, passwordMd5: ByteArray, phoneNumber: String = "") - constructor(id: Long, passwordPlainText: String, phoneNumber: String = "") +internal class BotAccount( + internal val id: Long, + val authorization: BotAuthorization, +) { + @TestOnly // to be compatible with your local tests :) + constructor( + id: Long, pwd: String + ) : this(id, BotAuthorization.byPassword(pwd)) - val passwordMd5: ByteArray + var accountSecretsKeyBuffer: SecretsProtection.EscapedByteBuffer? = null + + val accountSecretsKey: ByteArray + get() { + accountSecretsKeyBuffer?.let { return it.asByteArray } + error("accountSecretsKey not yet available") + } - override fun equals(other: Any?): Boolean - override fun hashCode(): Int } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/BotFactory.kt b/mirai-core/src/commonMain/kotlin/BotFactory.kt index 37bbfd85d..189ab8176 100644 --- a/mirai-core/src/commonMain/kotlin/BotFactory.kt +++ b/mirai-core/src/commonMain/kotlin/BotFactory.kt @@ -15,6 +15,7 @@ package net.mamoe.mirai.internal import net.mamoe.mirai.Bot import net.mamoe.mirai.BotFactory +import net.mamoe.mirai.auth.BotAuthorization import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.DeprecatedSinceMirai @@ -28,7 +29,7 @@ internal object BotFactoryImpl : BotFactory { * 使用指定的 [配置][configuration] 构造 [Bot] 实例 */ override fun newBot(qq: Long, password: String, configuration: BotConfiguration): Bot { - return QQAndroidBot(BotAccount(qq, password), configuration) + return QQAndroidBot(BotAccount(qq, BotAuthorization.byPassword(password)), configuration) } /** @@ -38,5 +39,9 @@ internal object BotFactoryImpl : BotFactory { qq: Long, passwordMd5: ByteArray, configuration: BotConfiguration - ): Bot = QQAndroidBot(BotAccount(qq, passwordMd5), configuration) + ): Bot = QQAndroidBot(BotAccount(qq, BotAuthorization.byPassword(passwordMd5)), configuration) + + override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot { + return QQAndroidBot(BotAccount(qq, authorization), configuration) + } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt index 55c4e5c58..ac765f6c7 100644 --- a/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt +++ b/mirai-core/src/commonMain/kotlin/QQAndroidBot.kt @@ -209,6 +209,10 @@ internal open class QQAndroidBot constructor( set(SsoProcessorContext, SsoProcessorContextImpl(bot)) set(SsoProcessor, SsoProcessorImpl(get(SsoProcessorContext))) + set( + QRCodeLoginProcessor, + QRCodeLoginProcessor.parse(get(SsoProcessorContext), networkLogger.subLogger("QRCodeLoginProcessor")) + ) val cacheValidator = CacheValidatorImpl( get(SsoProcessorContext), diff --git a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt index 5fbcf81c9..b858aab1c 100644 --- a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt +++ b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt @@ -169,7 +169,7 @@ internal open class QQAndroidClient( var t547: ByteArray? = null } -internal val QQAndroidClient.apkId: ByteArray get() = "com.tencent.mobileqq".toByteArray() +internal val QQAndroidClient.apkId: ByteArray get() = protocol.apkId.toByteArray() internal val QQAndroidClient.ssoVersion: Int get() = protocol.ssoVersion internal val QQAndroidClient.networkType: NetworkType get() = NetworkType.WIFI internal val QQAndroidClient.appClientVersion: Int get() = 0 diff --git a/mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt b/mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt new file mode 100644 index 000000000..cee451327 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt @@ -0,0 +1,116 @@ +/* + * 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.BotAuthorization +import net.mamoe.mirai.internal.network.components.SsoProcessorImpl +import net.mamoe.mirai.internal.utils.subLogger +import net.mamoe.mirai.utils.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.cancellation.CancellationException + + +/** + * Event sequence: + * + * 1. Starts a user coroutine [BotAuthorization.authorize]. + * 2. User coroutine + */ +internal class AuthControl( + private val botAuthInfo: BotAuthInfo, + private val authorization: BotAuthorization, + private val logger: MiraiLogger, + parentCoroutineContext: CoroutineContext, +) { + internal val exceptionCollector = ExceptionCollector() + + private val userDecisions: OnDemandConsumer = + CoroutineOnDemandValueScope(parentCoroutineContext, logger.subLogger("AuthControl/UserDecisions")) { _ -> + /** + * Implements [BotAuthSessionInternal] from API, to be called by the user, to receive user's decisions. + */ + val sessionImpl = object : BotAuthSessionInternal() { + private val authResultImpl = object : BotAuthResult {} + + override suspend fun authByPassword(passwordMd5: SecretsProtection.EscapedByteBuffer): BotAuthResult { + runWrapInternalException { + emit(SsoProcessorImpl.AuthMethod.Pwd(passwordMd5)) + }?.let { throw it } + return authResultImpl + } + + override suspend fun authByQRCode(): BotAuthResult { + runWrapInternalException { + emit(SsoProcessorImpl.AuthMethod.QRCode) + }?.let { throw it } + return authResultImpl + } + + private inline fun runWrapInternalException(block: () -> R): R { + try { + return block() + } catch (e: IllegalProducerStateException) { + if (e.lastStateWasSucceed) { + throw IllegalStateException( + "This login session has already completed. Please return the BotAuthResult you get from 'authBy*()' immediately", + e + ) + } else { + throw e // internal bug + } + } + } + } + + try { + logger.verbose { "[AuthControl/auth] Authorization started" } + + authorization.authorize(sessionImpl, botAuthInfo) + + logger.verbose { "[AuthControl/auth] Authorization exited" } + finish() + } catch (e: CancellationException) { + logger.verbose { "[AuthControl/auth] Authorization cancelled" } + } catch (e: Throwable) { + logger.verbose { "[AuthControl/auth] Authorization failed: $e" } + finishExceptionally(e) + } + } + + init { + userDecisions.expectMore(null) + } + + // Does not throw + suspend fun acquireAuth(): SsoProcessorImpl.AuthMethod { + logger.verbose { "[AuthControl/acquire] Acquiring auth method" } + + val rsp = try { + userDecisions.receiveOrNull() ?: SsoProcessorImpl.AuthMethod.NotAvailable + } catch (e: ProducerFailureException) { + SsoProcessorImpl.AuthMethod.Error(e) + } + + logger.debug { "[AuthControl/acquire] Authorization responded: $rsp" } + return rsp + } + + fun actMethodFailed(cause: Throwable) { + logger.verbose { "[AuthControl/resume] Fire auth failed with cause: $cause" } + userDecisions.expectMore(cause) + } + + fun actComplete() { + logger.verbose { "[AuthControl/resume] Fire auth completed" } + userDecisions.finish() + } +} diff --git a/mirai-core/src/commonMain/kotlin/network/auth/BotAuthSession.kt b/mirai-core/src/commonMain/kotlin/network/auth/BotAuthSession.kt new file mode 100644 index 000000000..608041424 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/auth/BotAuthSession.kt @@ -0,0 +1,51 @@ +/* + * 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.utils.SecretsProtection +import net.mamoe.mirai.utils.md5 + + +// With SecretsProtection support +internal abstract class BotAuthSessionInternal : BotAuthSession { + + final override suspend fun authByPassword(password: String): BotAuthResult { + return authByPassword(password.md5()) + } + + final override suspend fun authByPassword(passwordMd5: ByteArray): BotAuthResult { + return authByPassword(SecretsProtection.EscapedByteBuffer(passwordMd5)) + } + + abstract suspend fun authByPassword(passwordMd5: SecretsProtection.EscapedByteBuffer): BotAuthResult +} + +// With SecretsProtection support +internal abstract class BotAuthorizationWithSecretsProtection : BotAuthorization { + final override fun calculateSecretsKey(bot: BotAuthInfo): ByteArray { + return calculateSecretsKeyImpl(bot).asByteArray + } + + abstract fun calculateSecretsKeyImpl( + bot: BotAuthInfo, + ): SecretsProtection.EscapedByteBuffer + + abstract suspend fun authorize(session: BotAuthSessionInternal, bot: BotAuthInfo): BotAuthResult + + final override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult { + return authorize(session as BotAuthSessionInternal, info) + } +} + + diff --git a/mirai-core/src/commonMain/kotlin/network/auth/CoroutineOnDemandValueScope.kt b/mirai-core/src/commonMain/kotlin/network/auth/CoroutineOnDemandValueScope.kt new file mode 100644 index 000000000..71ba2169e --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/auth/CoroutineOnDemandValueScope.kt @@ -0,0 +1,203 @@ +/* + * 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.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.cancel +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.childScope +import net.mamoe.mirai.utils.debug +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.cancellation.CancellationException + + +internal class IllegalProducerStateException( + private val state: ProducerState<*, *>, + message: String? = state.toString(), + cause: Throwable? = null, +) : IllegalStateException(message, cause) { + val lastStateWasSucceed get() = (state is ProducerState.Finished) && state.isSuccess +} + +internal class CoroutineOnDemandValueScope( + parentCoroutineContext: CoroutineContext, + private val logger: MiraiLogger, + private val producerCoroutine: suspend OnDemandProducerScope.(initialTicket: T) -> Unit, +) : OnDemandConsumer { + private val coroutineScope = parentCoroutineContext.childScope("CoroutineOnDemandValueScope") + + private val state: AtomicRef> = atomic(ProducerState.JustInitialized()) + + + inner class Producer( + private val initialTicket: T, + ) : OnDemandProducerScope { + init { + coroutineScope.launch { + try { + producerCoroutine(initialTicket) + } catch (_: CancellationException) { + // ignored + } catch (e: Exception) { + finishExceptionally(e) + } + } + } + + override suspend fun emit(value: V): T { + state.loop { state -> + when (state) { + is ProducerState.Finished -> throw state.createAlreadyFinishedException(null) + is ProducerState.Producing -> { + val deferred = state.deferred + val consumingState = ProducerState.Consuming( + state.producer, + state.deferred, + coroutineScope.coroutineContext + ) + if (compareAndSetState(state, consumingState)) { + deferred.complete(value) // produce a value + return consumingState.producerLatch.acquire() // wait for producer to consume the previous value. + } + } + + else -> throw IllegalProducerStateException(state) + } + } + } + + override fun finishExceptionally(exception: Throwable) { + finishImpl(exception) + } + + override fun finish() { + state.loop { state -> + when (state) { + is ProducerState.Finished -> throw state.createAlreadyFinishedException(null) + else -> { + if (compareAndSetState(state, ProducerState.Finished(state, null))) { + return + } + } + } + } + } + } + + private fun finishImpl(exception: Throwable?) { + state.loop { state -> + when (state) { + is ProducerState.Finished -> throw state.createAlreadyFinishedException(exception) + else -> { + if (compareAndSetState(state, ProducerState.Finished(state, exception))) { + val cancellationException = kotlinx.coroutines.CancellationException("Finished", exception) + coroutineScope.cancel(cancellationException) + return + } + } + } + } + } + + private fun compareAndSetState(state: ProducerState, newState: ProducerState): Boolean { + return this.state.compareAndSet(state, newState).also { + logger.debug { "CAS: $state -> $newState: $it" } + } + } + + override suspend fun receiveOrNull(): V? { + state.loop { state -> + when (state) { + is ProducerState.Producing -> { + // still producing value + + state.deferred.await() // just wait for value, but does not return it. + + // The value will be completed in ProducerState.Consuming state, + // but you cannot thread-safely assume current state is Consuming. + + // Here we will loop again, to atomically switch to Consumed state. + } + + is ProducerState.Consuming -> { + // value is ready, switch state to ProducerReady + + if (compareAndSetState( + state, + ProducerState.Consumed(state.producer, state.producerLatch) + ) + ) { + return try { + state.value.await() // won't suspend, since value is already completed + } catch (e: Exception) { + throw ProducerFailureException(cause = e) + } + } + } + + is ProducerState.Finished -> { + state.exception?.let { err -> + throw ProducerFailureException(cause = err) + } + return null + } + else -> throw IllegalProducerStateException(state) + } + } + } + + override fun expectMore(ticket: T): Boolean { + state.loop { state -> + when (state) { + is ProducerState.JustInitialized -> { + compareAndSetState(state, ProducerState.CreatingProducer { Producer(ticket) }) + // loop again + } + + is ProducerState.CreatingProducer -> { + compareAndSetState(state, ProducerState.ProducerReady(state.producer)) + // loop again + } + + is ProducerState.ProducerReady -> { + val deferred = CompletableDeferred(coroutineScope.coroutineContext.job) + if (!compareAndSetState(state, ProducerState.Producing(state.producer, deferred))) { + deferred.cancel() // avoid leak + } + // loop again + } + + is ProducerState.Producing -> return true // ok + + is ProducerState.Consuming -> throw IllegalProducerStateException(state) // a value is already ready + + is ProducerState.Consumed -> { + if (compareAndSetState(state, ProducerState.ProducerReady(state.producer))) { + // wake up producer async. + state.producerLatch.resumeWith(Result.success(ticket)) + // loop again to switch state atomically to Producing. + // Do not do switch state directly here — async producer may race with you! + } + } + + is ProducerState.Finished -> return false + } + } + } + + override fun finish() { + finishImpl(null) + } +} diff --git a/mirai-core/src/commonMain/kotlin/network/auth/DefaultBotAuthorizationFactoryImpl.kt b/mirai-core/src/commonMain/kotlin/network/auth/DefaultBotAuthorizationFactoryImpl.kt new file mode 100644 index 000000000..dbe04f8b7 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/auth/DefaultBotAuthorizationFactoryImpl.kt @@ -0,0 +1,51 @@ +/* + * 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.utils.SecretsProtection.EscapedByteBuffer + +/** + * Provides default [BotAuthorization.byPassword] implementation. + * @see net.mamoe.mirai.auth.DefaultBotAuthorizationFactory + */ +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER") +internal class DefaultBotAuthorizationFactoryImpl : + net.mamoe.mirai.auth.DefaultBotAuthorizationFactory { + override fun byPassword(passwordMd5: ByteArray): BotAuthorization { + val buffer = EscapedByteBuffer(passwordMd5) + return byPassword(buffer) // Avoid referring passwordMd5(ByteArray) + } + + private fun byPassword(buffer: EscapedByteBuffer): BotAuthorization { + return object : BotAuthorizationWithSecretsProtection() { + override fun calculateSecretsKeyImpl(bot: BotAuthInfo): EscapedByteBuffer = buffer + + override suspend fun authorize( + session: BotAuthSessionInternal, + bot: BotAuthInfo + ): BotAuthResult = session.authByPassword(buffer) + + override fun toString(): String = "BotAuthorization.byPassword()" + } + } + + override fun byQRCode(): BotAuthorization { + return object : BotAuthorization { + override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult = + session.authByQRCode() + + override fun toString(): String = "BotAuthorization.byQRCode()" + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/auth/Latch.kt b/mirai-core/src/commonMain/kotlin/network/auth/Latch.kt new file mode 100644 index 000000000..1754f5cd7 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/auth/Latch.kt @@ -0,0 +1,51 @@ +/* + * 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.CompletableDeferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.completeWith +import kotlin.coroutines.CoroutineContext + + +internal interface Latch { + /** + * Suspends and waits to acquire the latch. + * @throws Throwable if [resumeWith] is called with [Result.Failure] + */ + suspend fun acquire(): T + + /** + * Release the latch, resuming the coroutines waiting for the latch. + * + * This function will return immediately unless a client is calling [acquire] concurrently. + */ + fun resumeWith(result: Result) +} + + +internal fun Latch(parentCoroutineContext: CoroutineContext): Latch = LatchImpl(parentCoroutineContext) + +private class LatchImpl( + parentCoroutineContext: CoroutineContext +) : Latch { + private val deferred: CompletableDeferred = CompletableDeferred(parentCoroutineContext[Job]) + + + override suspend fun acquire(): T = deferred.await() + + override fun resumeWith(result: Result) { + if (!deferred.completeWith(result)) { + error("$this was already resumed") + } + } + + override fun toString(): String = "LatchImpl($deferred)" +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/auth/OnDemandValueScope.kt b/mirai-core/src/commonMain/kotlin/network/auth/OnDemandValueScope.kt new file mode 100644 index 000000000..efad0b37f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/auth/OnDemandValueScope.kt @@ -0,0 +1,86 @@ +/* + * 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.* +import kotlinx.coroutines.channels.ReceiveChannel +import kotlin.coroutines.Continuation +import kotlin.coroutines.cancellation.CancellationException + + +/** + * 按需供给的值制造器. + */ +internal interface OnDemandProducerScope { + /** + * 挂起协程, 直到 [OnDemandConsumer] 期望接收一个 [V], 届时将 [value] 传递给 [OnDemandConsumer.receiveOrNull], 成为其返回值. + * + * 若在调用 [emit] 时已经有 [OnDemandConsumer] 正在等待, 则该 [OnDemandConsumer] 协程会立即[恢复][Continuation.resumeWith]. + * + * 若 [OnDemandConsumer] 已经[完结][OnDemandConsumer.finish], [OnDemandProducerScope.emit] 会抛出 [IllegalProducerStateException]. + */ + suspend fun emit(value: V): T + + /** + * 标记此 [OnDemandProducerScope] 在生产 [V] 的过程中出现错误. + * + * 这也会终止此 [OnDemandProducerScope], 随后 [OnDemandConsumer.receiveOrNull] 将会抛出 [ProducerFailureException]. + */ + fun finishExceptionally(exception: Throwable) + + /** + * 标记此 [OnDemandProducerScope] 已经没有更多 [V] 可生产. + * + * 随后 [OnDemandConsumer.receiveOrNull] 将会抛出 [IllegalStateException]. + */ + fun finish() +} + +/** + * 按需消费者. + * + * 与 [ReceiveChannel] 不同, [OnDemandConsumer] 只有在调用 [expectMore] 后才会期待[生产者][OnDemandProducerScope] 生产下一个 [V]. + */ +internal interface OnDemandConsumer { + /** + * 挂起协程并等待从 [OnDemandProducerScope] [接收][OnDemandProducerScope.emit]一个 [V]. + * + * 当此函数被多个线程 (协程) 同时调用时, 只有一个线程挂起并获得 [V], 其他线程将会 + * + * @throws ProducerFailureException 当 [OnDemandProducerScope.finishExceptionally] 时抛出. + * @throws CancellationException 当协程被取消时抛出 + * @throws IllegalProducerStateException 当状态异常, 如未调用 [expectMore] 时抛出 + */ + @Throws(ProducerFailureException::class, CancellationException::class) + suspend fun receiveOrNull(): V? + + /** + * 期待 [OnDemandProducerScope] 再生产一个 [V]. 期望生产后必须在之后调用 [receiveOrNull] 或 [finish] 来消耗生产的 [V]. + * + * 在成功发起期待后返回 `true`; 在 [OnDemandProducerScope] 已经[完结][OnDemandProducerScope.finish] 时返回 `false`. + * + * @throws IllegalProducerStateException 当 [expectMore] 被调用后, 没有调用 [receiveOrNull] 就又调用了 [expectMore] 时抛出 + */ + fun expectMore(ticket: T): Boolean + + /** + * 标记此 [OnDemandConsumer] 已经完结. + * + * 如果 [OnDemandProducerScope] 仍在运行, 将会 (正常地) 取消 [OnDemandProducerScope]. + * + * 随后 [OnDemandProducerScope.emit] 将会抛出 [IllegalStateException]. + */ + fun finish() +} + +internal class ProducerFailureException( + override val message: String? = null, + override val cause: Throwable? +) : Exception() diff --git a/mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt b/mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt new file mode 100644 index 000000000..805304382 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt @@ -0,0 +1,176 @@ +/* + * 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.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlin.coroutines.CoroutineContext + +/** + * Producer states. + */ +internal sealed interface ProducerState { + /* + * 可变更状态的函数: [emit], [receiveOrNull], [expectMore], [finish], [finishExceptionally] + * + * [emit] 和 [receiveOrNull] 为 suspend 函数, 在图中 "(suspend)" 表示挂起它们的协程, "(resume)" 表示恢复它们的协程. + * + * "A ~~~~~~> B" 表示在切换为状态 A 后, 会挂起或恢复协程 B. + * + * + * + * + * JustInitialized + * | + * | 调用 [expectMore] + * | + * V + * CreatingProducer + * | + * | + * | + * V + * ProducerReady (从此用户协程作为 producer 在后台运行) + * | + * | + * | <-------------------------------------------------- + * | \ + * V | + * Producing ([expectMore] 结束) | + * | \ | + * 调用 | \ | + * [receiveOrNull] | \ 调用 [emit] | + * / \ | + * / \ | + * / \ | + * | \ | + * | \ | + * | |------------- | + * | | \ | + * | | | | + * | \ | | + * | \ | | + * | \ | | + * | | | | + * V (resume) V | | + * ([receiveOrNull] suspend) <~~~~~~~~~~~~ Consuming | | + * | / | | + * | / | | + * | /---------------/ | | + * | / 调用 [receiveOrNull] | | + * | / | | + * |/ | | + * | | | + * | | | + * V | | + * ([receiveOrNull] 结束) Consumed | | + * | | | + * | 调用 [expectMore] | | + * | | | + * V (resume) V | + * ProducerReady ~~~~~~~~~~~~~~~~> ([emit] suspend) | + * | | | + * | | | + * | V | + * | ([emit] 结束) | + * | | + * |------------------------------------------------------------+ + * (返回顶部 Producing) + * + * + * + * 在任意状态调用 [finish] 以及 [finishExceptionally], 可将状态转移到最终状态 [Finished]. + * + * 在一个状态中调用图中未说明的函数会抛出 [IllegalProducerStateException]. + */ + + /** + * Override this function to produce good debug information + */ + abstract override fun toString(): String + + class JustInitialized : ProducerState { + override fun toString(): String = "JustInitialized" + } + + sealed interface HasProducer : ProducerState { + val producer: OnDemandProducerScope + } + + // This is need — to ensure [launchProducer] is called exactly once. + class CreatingProducer( + launchProducer: () -> OnDemandProducerScope + ) : HasProducer { + override val producer: OnDemandProducerScope by lazy(launchProducer) + override fun toString(): String = "CreatingProducer" + } + + class ProducerReady( + override val producer: OnDemandProducerScope, + ) : HasProducer { + override fun toString(): String = "ProducerReady" + } + + class Producing( + override val producer: OnDemandProducerScope, + val deferred: CompletableDeferred, + ) : HasProducer { + override fun toString(): String = "Producing(deferred.completed=${deferred.isCompleted})" + } + + class Consuming( + override val producer: OnDemandProducerScope, + val value: Deferred, + parentCoroutineContext: CoroutineContext, + ) : HasProducer { + val producerLatch = Latch(parentCoroutineContext) + + override fun toString(): String { + val completed = + value.runCatching { getCompleted().toString() }.getOrNull() // getCompleted() is experimental + return "Consuming(value=$completed)" + } + } + + class Consumed( + override val producer: OnDemandProducerScope, + val producerLatch: Latch + ) : HasProducer { + override fun toString(): String = "Consumed($producerLatch)" + } + + class Finished( + val previousState: ProducerState, + val exception: Throwable?, + ) : ProducerState { + val isSuccess get() = exception == null + + fun createAlreadyFinishedException(cause: Throwable?): IllegalProducerStateException { + val exception = exception + return if (exception == null) { + IllegalProducerStateException( + this, + "Producer has already finished normally, but attempting to finish with the cause $cause. Previous state was: $previousState", + cause = cause + ) + } else { + IllegalProducerStateException( + this, + "Producer has already finished with the suppressed exception, but attempting to finish with the cause $cause. Previous state was: $previousState", + cause = cause + ).apply { + addSuppressed(exception) + } + } + } + + override fun toString(): String = "Finished($previousState, $exception)" + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt b/mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt index 4ccd3d5c7..cb64a8f8a 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/AccountSecretsManager.kt @@ -193,7 +193,7 @@ internal class FileCacheAccountSecretsManager( private fun getSecretsImpl(account: BotAccount): AccountSecrets? { if (!file.exists()) return null val loaded = kotlin.runCatching { - TEA.decrypt(file.readBytes(), account.passwordMd5).loadAs(AccountSecretsImpl.serializer()) + TEA.decrypt(file.readBytes(), account.accountSecretsKey).loadAs(AccountSecretsImpl.serializer()) }.getOrElse { e -> if (e.message == "Field 'ecdhInitialPublicKey' is required for type with serial name 'net.mamoe.mirai.internal.network.components.AccountSecretsImpl', but it was missing") { logger.info { "Detected old account secrets, invalidating..." } @@ -218,7 +218,7 @@ internal class FileCacheAccountSecretsManager( file.writeBytes( TEA.encrypt( AccountSecretsImpl(secrets).toByteArray(AccountSecretsImpl.serializer()), - account.passwordMd5 + account.accountSecretsKey ) ) } diff --git a/mirai-core/src/commonMain/kotlin/network/components/BotClientHolder.kt b/mirai-core/src/commonMain/kotlin/network/components/BotClientHolder.kt index 25ee465f9..2beb02b0a 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/BotClientHolder.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/BotClientHolder.kt @@ -18,6 +18,8 @@ import net.mamoe.mirai.utils.lateinitMutableProperty internal interface BotClientHolder { var client: QQAndroidClient + fun refreshClient() + companion object : ComponentKey } @@ -27,6 +29,10 @@ internal class BotClientHolderImpl( ) : BotClientHolder { override var client: QQAndroidClient by lateinitMutableProperty { createClient(bot) } + override fun refreshClient() { + client = createClient(bot) + } + private fun createClient(bot: QQAndroidBot): QQAndroidClient { val ssoContext = bot.components[SsoProcessorContext] val device = ssoContext.device diff --git a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt index dd9fc306d..3cd1fa143 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt @@ -17,6 +17,7 @@ import net.mamoe.mirai.internal.network.components.PacketCodec.Companion.PacketL import net.mamoe.mirai.internal.network.components.PacketCodecException.Kind.* import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.protocol.packet.* +import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin import net.mamoe.mirai.internal.utils.crypto.Ecdh import net.mamoe.mirai.internal.utils.crypto.TEA import net.mamoe.mirai.utils.* @@ -96,57 +97,80 @@ internal class PacketCodecException( internal class PacketCodecImpl : PacketCodec { - override fun decodeRaw(client: SsoSession, input: ByteReadPacket): RawIncomingPacket = input.run { - // login - val flag1 = readInt() + override fun decodeRaw( + client: SsoSession, + input: ByteReadPacket + ): RawIncomingPacket = input.run { + // packet type + val type = readInt() PacketLogger.verbose { "开始处理一个包" } - val flag2 = readByte().toInt() + val encryptMethod = readByte().toInt() val flag3 = readByte().toInt() - if (flag3 != 0) { - throw PacketCodecException( - "Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " + - "Remaining=${this.readBytes().toUHexString()}", + val flag3Exception = if (flag3 != 0) { + PacketCodecException( + "Illegal flag3. Expected 0, whereas got $flag3. packet type=$type, encrypt method=$encryptMethod. ", kind = PROTOCOL_UPDATED ) - } + } else null readString(readInt() - 4)// uinAccount ByteArrayPool.useInstance(this.remaining.toInt()) { buffer -> val size = this.readAvailable(buffer) - when (flag2) { - 2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size) - 1 -> TEA.decrypt(buffer, client.wLoginSigInfo.d2Key, size) - 0 -> buffer - else -> throw PacketCodecException("Unknown flag2=$flag2", PROTOCOL_UPDATED) - }.let { decryptedData -> - when (flag1) { - 0x0A -> parseSsoFrame(client, decryptedData) - 0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样. - else -> throw PacketCodecException( - "unknown flag1: ${flag1.toByte().toUHexString()}", - PROTOCOL_UPDATED - ) + val raw = try { + when (encryptMethod) { + 2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size) + 1 -> TEA.decrypt(buffer, client.wLoginSigInfo.d2Key, size) + 0 -> buffer + else -> throw PacketCodecException("Unknown encrypt type=$encryptMethod", PROTOCOL_UPDATED) + }.let { decryptedData -> + when (type) { + 0x0A -> parseSsoFrame(client, decryptedData) + 0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样. + else -> throw PacketCodecException( + "unknown packet type: ${type.toByte().toUHexString()}", + PROTOCOL_UPDATED + ) + } } - }.let { raw -> - when (flag2) { - 0, 1 -> RawIncomingPacket(raw.commandName, raw.sequenceId, raw.body.readBytes()) - 2 -> RawIncomingPacket( - raw.commandName, - raw.sequenceId, - raw.body.withUse { - try { - parseOicqResponse(client) - } catch (e: Throwable) { - throw PacketCodecException(e, PacketCodecException.Kind.OTHER) - } + } catch (e: Exception) { + throw e.also { + if (flag3Exception != null) { + it.addSuppressed(flag3Exception) + } + } + } + + if (flag3 != 0 && flag3Exception != null) { + if (raw.commandName == WtLogin.TransEmp.commandName) { + PacketLogger.warning( + "unknown flag3: $flag3 in packet ${WtLogin.TransEmp.commandName}, " + + "which may means protocol is updated.", + flag3Exception + ) + } else { + throw flag3Exception + } + } + + when (encryptMethod) { + 0, 1 -> RawIncomingPacket(raw.commandName, raw.sequenceId, raw.body.readBytes()) + 2 -> RawIncomingPacket( + raw.commandName, + raw.sequenceId, + raw.body.withUse { + try { + parseOicqResponse(client) + } catch (e: Throwable) { + throw PacketCodecException(e, PacketCodecException.Kind.OTHER) } - ) - else -> error("unreachable") - } + } + ) + + else -> error("unreachable") } } } @@ -220,6 +244,7 @@ internal class PacketCodecImpl : PacketCodec { } } } + 1 -> { input.discardExact(4) input.inflateAllAvailable().let { bytes -> @@ -231,6 +256,7 @@ internal class PacketCodecImpl : PacketCodec { } } } + 8 -> input else -> throw PacketCodecException("Unknown dataCompressed flag: $dataCompressed", PROTOCOL_UPDATED) } @@ -270,6 +296,7 @@ internal class PacketCodecImpl : PacketCodec { qqEcdh.calculateQQShareKey(Ecdh.Instance.importPublicKey(readUShortLVByteArray())) TEA.decrypt(data, peerShareKey) } + 3 -> { val size = (this.remaining - 1).toInt() // session @@ -279,6 +306,7 @@ internal class PacketCodecImpl : PacketCodec { length = size ) } + 0 -> { if (client.loginState == 0) { val size = (this.remaining - 1).toInt() @@ -294,6 +322,7 @@ internal class PacketCodecImpl : PacketCodec { TEA.decrypt(this.readBytes(), client.randomKey, length = size) } } + else -> error("Illegal encryption method. expected 0 or 4, got $encryptionMethod") } } diff --git a/mirai-core/src/commonMain/kotlin/network/components/QRCodeLoginProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/QRCodeLoginProcessor.kt new file mode 100644 index 000000000..0ac1de817 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/components/QRCodeLoginProcessor.kt @@ -0,0 +1,167 @@ +/* + * 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.components + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.delay +import net.mamoe.mirai.auth.QRCodeLoginListener +import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.QRCodeLoginData +import net.mamoe.mirai.internal.network.component.ComponentKey +import net.mamoe.mirai.internal.network.handler.NetworkHandler +import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin +import net.mamoe.mirai.internal.utils.MiraiProtocolInternal.Companion.asInternal +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.debug + +internal interface QRCodeLoginProcessor { + suspend fun process(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginData = error("Not implemented") + + /** + * Allocate a special processor for once login request + */ + fun prepareProcess(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginProcessor = + error("Not implemented") + + companion object : ComponentKey { + internal val NOOP = object : QRCodeLoginProcessor {} + + fun parse(ssoContext: SsoProcessorContext, logger: MiraiLogger): QRCodeLoginProcessor { + return QRCodeLoginProcessorPreLoaded(ssoContext, logger) + } + } +} + +internal class QRCodeLoginProcessorPreLoaded( + private val ssoContext: SsoProcessorContext, + private val logger: MiraiLogger, +) : QRCodeLoginProcessor { + override fun prepareProcess(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginProcessor { + check(ssoContext.bot.configuration.protocol.asInternal.supportsQRLogin) { + "The login protocol must be ANDROID_WATCH or MACOS while enabling qrcode login." + + "Set it by `bot.configuration.protocol = BotConfiguration.MiraiProtocol.ANDROID_WATCH`." + } + + val loginSolver = ssoContext.bot.configuration.loginSolver + ?: throw IllegalStateException( + "No LoginSolver found while enabling qrcode login. " + + "Please provide by BotConfiguration.loginSolver. " + + "For example use `BotFactory.newBot(...) { loginSolver = yourLoginSolver}` in Kotlin, " + + "use `BotFactory.newBot(..., new BotConfiguration() {{ setLoginSolver(yourLoginSolver) }})` in Java." + ) + + val qrCodeLoginListener = loginSolver.createQRCodeLoginListener(client.bot) + + return loginSolver.run { + QRCodeLoginProcessorImpl(qrCodeLoginListener, logger) + } + } +} + +internal class QRCodeLoginProcessorImpl( + private val qrCodeLoginListener: QRCodeLoginListener, + private val logger: MiraiLogger, +) : QRCodeLoginProcessor { + + private var state = atomic(QRCodeLoginListener.State.DEFAULT) + + private suspend fun requestQRCode( + handler: NetworkHandler, + client: QQAndroidClient + ): WtLogin.TransEmp.Response.FetchQRCode { + logger.debug { "requesting qrcode." } + val resp = handler.sendAndExpect( + WtLogin.TransEmp.FetchQRCode( + client, + size = qrCodeLoginListener.qrCodeSize, + margin = qrCodeLoginListener.qrCodeMargin, + ecLevel = qrCodeLoginListener.qrCodeEcLevel, + ), + attempts = 1 + ) + check(resp is WtLogin.TransEmp.Response.FetchQRCode) { "Cannot fetch qrcode, resp=$resp" } + qrCodeLoginListener.onFetchQRCode(handler.context.bot, resp.imageData) + return resp + } + + private suspend fun queryQRCodeStatus( + handler: NetworkHandler, + client: QQAndroidClient, + sig: ByteArray + ): WtLogin.TransEmp.Response { + logger.debug { "querying qrcode state." } + val resp = handler.sendAndExpect(WtLogin.TransEmp.QueryQRCodeStatus(client, sig), attempts = 1, timeout = 500) + check( + resp is WtLogin.TransEmp.Response.QRCodeStatus || resp is WtLogin.TransEmp.Response.QRCodeConfirmed + ) { "Cannot query qrcode status, resp=$resp" } + + val currentState = state.value + val newState = resp.mapProtocolState() + if (currentState != newState && state.compareAndSet(currentState, newState)) { + logger.debug { "qrcode state changed: $state" } + qrCodeLoginListener.onStateChanged(handler.context.bot, newState) + } + return resp + } + + override suspend fun process(handler: NetworkHandler, client: QQAndroidClient): QRCodeLoginData { + main@ while (true) { + val qrCodeData = requestQRCode(handler, client) + state@ while (true) { + qrCodeLoginListener.onIntervalLoop() + + when (val status = queryQRCodeStatus(handler, client, qrCodeData.sig)) { + is WtLogin.TransEmp.Response.QRCodeConfirmed -> { + return status.data + } + + is WtLogin.TransEmp.Response.QRCodeStatus -> when (status.state) { + WtLogin.TransEmp.Response.QRCodeStatus.State.TIMEOUT, + WtLogin.TransEmp.Response.QRCodeStatus.State.CANCELLED -> { + break@state + } + + else -> {} // WAITING_FOR_SCAN or WAITING_FOR_CONFIRM + } + // status is FetchQRCode, which is unreachable. + else -> { + error("query qrcode status should not be FetchQRCode.") + } + } + + delay(qrCodeLoginListener.qrCodeStateUpdateInterval.coerceAtLeast(200L)) + } + } + } + + private fun WtLogin.TransEmp.Response.mapProtocolState(): QRCodeLoginListener.State { + return when (this) { + is WtLogin.TransEmp.Response.QRCodeStatus -> when (this.state) { + WtLogin.TransEmp.Response.QRCodeStatus.State.WAITING_FOR_SCAN -> + QRCodeLoginListener.State.WAITING_FOR_SCAN + + WtLogin.TransEmp.Response.QRCodeStatus.State.WAITING_FOR_CONFIRM -> + QRCodeLoginListener.State.WAITING_FOR_CONFIRM + + WtLogin.TransEmp.Response.QRCodeStatus.State.CANCELLED -> + QRCodeLoginListener.State.CANCELLED + + WtLogin.TransEmp.Response.QRCodeStatus.State.TIMEOUT -> + QRCodeLoginListener.State.TIMEOUT + } + + is WtLogin.TransEmp.Response.QRCodeConfirmed -> + QRCodeLoginListener.State.CONFIRMED + + is WtLogin.TransEmp.Response.FetchQRCode -> + error("$this cannot be mapped to listener state.") + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt index 76693ec53..2371b43fb 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -11,13 +11,18 @@ package net.mamoe.mirai.internal.network.components import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic -import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.auth.* import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.QRCodeLoginData import net.mamoe.mirai.internal.network.WLoginSigInfo +import net.mamoe.mirai.internal.network.auth.AuthControl +import net.mamoe.mirai.internal.network.auth.BotAuthorizationWithSecretsProtection import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.network.handler.NetworkHandler +import net.mamoe.mirai.internal.network.handler.logger import net.mamoe.mirai.internal.network.handler.selector.NetworkException +import net.mamoe.mirai.internal.network.handler.selector.SelectorRequireReconnectException import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType import net.mamoe.mirai.internal.network.protocol.packet.login.DeviceVerificationResultImpl import net.mamoe.mirai.internal.network.protocol.packet.login.SmsDeviceVerificationResult @@ -30,10 +35,8 @@ 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.utils.* import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol -import net.mamoe.mirai.utils.LoginSolver -import net.mamoe.mirai.utils.info -import net.mamoe.mirai.utils.withExceptionCollector import kotlin.coroutines.cancellation.CancellationException import kotlin.jvm.Volatile @@ -142,37 +145,155 @@ internal class SsoProcessorImpl( override val ssoSession: SsoSession get() = client private val components get() = ssoContext.bot.components + private val botAuthInfo = object : BotAuthInfo { + override val id: Long + get() = ssoContext.bot.id + override val deviceInfo: DeviceInfo + get() = ssoContext.device + override val configuration: BotConfiguration + get() = ssoContext.bot.configuration + } + /** * Do login. Throws [LoginFailedException] if failed */ - override suspend fun login(handler: NetworkHandler) = withExceptionCollector { - components[CacheValidator].validate() + override suspend fun login(handler: NetworkHandler) { - components[BdhSessionSyncer].loadServerListFromCache() - try { + fun initAuthControl() { + authControl = AuthControl( + botAuthInfo, + ssoContext.bot.account.authorization, + ssoContext.bot.network.logger, + ssoContext.bot.coroutineContext, // do not use network context because network may restart whilst auth control should keep alive + ) + } + + suspend fun loginSuccess() { + components[AccountSecretsManager].saveSecrets(ssoContext.account, AccountSecretsImpl(client)) + registerClientOnline(handler) + ssoContext.bot.logger.info { "Login successful." } + } + + if (authControl == null) { + ssoContext.bot.account.let { account -> + if (account.accountSecretsKeyBuffer == null) { + + account.accountSecretsKeyBuffer = when (val authorization = account.authorization) { + is BotAuthorizationWithSecretsProtection -> authorization.calculateSecretsKeyImpl(botAuthInfo) + else -> SecretsProtection.EscapedByteBuffer(authorization.calculateSecretsKey(botAuthInfo)) + } + } + } + + components[CacheValidator].validate() + + components[BdhSessionSyncer].loadServerListFromCache() + + // try fast login if (client.wLoginSigInfoInitialized) { ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh() kotlin.runCatching { FastLoginImpl(handler).doLogin() }.onFailure { e -> - collectException(e) - SlowLoginImpl(handler).doLogin() + initAuthControl() + authControl!!.exceptionCollector.collect(e) + + throw SelectorRequireReconnectException() } - } else { - client = createClient(ssoContext.bot) - ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh() - SlowLoginImpl(handler).doLogin() + + loginSuccess() + + return } - } catch (e: Exception) { - // Failed to log in, invalidate secrets. - ssoContext.bot.components[AccountSecretsManager].invalidate() - throw e } - components[AccountSecretsManager].saveSecrets(ssoContext.account, AccountSecretsImpl(client)) - registerClientOnline(handler) - ssoContext.bot.logger.info { "Login successful." } + + if (authControl == null) initAuthControl() + val authControl0 = authControl!! + + + var nextAuthMethod: AuthMethod? = null + try { + ssoContext.bot.components[BotClientHolder].refreshClient() + ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh() + + when (val authw = authControl0.acquireAuth().also { nextAuthMethod = it }) { + is AuthMethod.Error -> { + authControl = null + throw authw.exception + } + + AuthMethod.NotAvailable -> { + authControl = null + error("No more auth method available") + } + + is AuthMethod.Pwd -> { + SlowLoginImpl(handler, LoginType.Password(authw.passwordMd5)).doLogin() + } + + AuthMethod.QRCode -> { + val rsp = ssoContext.bot.components[QRCodeLoginProcessor].prepareProcess( + handler, client + ).process(handler, client) + + SlowLoginImpl(handler, LoginType.QRCode(rsp)).doLogin() + } + } + + authControl!!.actComplete() + authControl = null + } catch (exception: Throwable) { + if (exception is SelectorRequireReconnectException) { + throw exception + } + + ssoContext.bot.network.logger.warning({ "Failed with auth method: $nextAuthMethod" }, exception) + authControl0.exceptionCollector.collectException(exception) + + if (nextAuthMethod !is AuthMethod.Error && nextAuthMethod != null) { + authControl0.actMethodFailed(exception) + } + + if (exception is NetworkException) { + if (exception.recoverable) throw exception + } + + if (nextAuthMethod == null || nextAuthMethod is AuthMethod.NotAvailable || nextAuthMethod is AuthMethod.Error) { + authControl = null + authControl0.exceptionCollector.throwLast() + } + + throw SelectorRequireReconnectException() + } + + loginSuccess() + } + + sealed class AuthMethod { + object NotAvailable : AuthMethod() { + override fun toString(): String = "NotAvailable" + } + + object QRCode : AuthMethod() { + override fun toString(): String = "QRCode" + } + + class Pwd(val passwordMd5: SecretsProtection.EscapedByteBuffer) : AuthMethod() { + override fun toString(): String = "Password@${hashCode()}" + } + + /** + * Exception in [BotAuthorization] + */ + class Error(val exception: Throwable) : AuthMethod() { + override fun toString(): String = "Error[$exception]@${hashCode()}" + } + } + + private var authControl: AuthControl? = null + override suspend fun sendRegister(handler: NetworkHandler): StatSvc.Register.Response { return registerClientOnline(handler).also { registerResp = it } } @@ -189,17 +310,6 @@ internal class SsoProcessorImpl( } } - private fun createClient(bot: QQAndroidBot): QQAndroidClient { - val device = ssoContext.device - return QQAndroidClient( - ssoContext.account, - device = device, - accountSecrets = bot.components[AccountSecretsManager].getSecretsOrCreate(ssoContext.account, device) - ).apply { - _bot = bot - } - } - /////////////////////////////////////////////////////////////////////////// // login /////////////////////////////////////////////////////////////////////////// @@ -219,7 +329,10 @@ internal class SsoProcessorImpl( abstract suspend fun doLogin() } - private inner class SlowLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) { + private inner class SlowLoginImpl( + handler: NetworkHandler, + private val loginType: LoginType + ) : LoginStrategy(handler) { private fun loginSolverNotNull(): LoginSolver { fun LoginSolver?.notnull(): LoginSolver { @@ -259,9 +372,15 @@ internal class SsoProcessorImpl( override suspend fun doLogin() = withExceptionCollector { + @Suppress("FunctionName") + fun SSOWtLogin9(allowSlider: Boolean) = when (loginType) { + is LoginType.Password -> WtLogin9.Password(client, loginType.passwordMd5.asByteArray, allowSlider) + is LoginType.QRCode -> WtLogin9.QRCode(client, loginType.qrCodeLoginData) + } + var allowSlider = sliderSupported || bot.configuration.protocol == MiraiProtocol.ANDROID_PHONE - var response: LoginPacketResponse = WtLogin9(client, allowSlider).sendAndExpect() + var response: LoginPacketResponse = SSOWtLogin9(allowSlider).sendAndExpect() mainloop@ while (true) { when (response) { @@ -281,7 +400,7 @@ internal class SsoProcessorImpl( check(result is DeviceVerificationResultImpl) response = when (result) { is UrlDeviceVerificationResult -> { - WtLogin9(client, allowSlider).sendAndExpect() + SSOWtLogin9(allowSlider).sendAndExpect() } is SmsDeviceVerificationResult -> { @@ -308,7 +427,7 @@ internal class SsoProcessorImpl( collectThrow(error) } response = if (ticket == null) { - WtLogin9(client, allowSlider).sendAndExpect() + SSOWtLogin9(allowSlider).sendAndExpect() } else { WtLogin2.SubmitSliderCaptcha(client, ticket).sendAndExpect() } @@ -358,6 +477,11 @@ internal class SsoProcessorImpl( } } + private sealed class LoginType { + class Password(val passwordMd5: SecretsProtection.EscapedByteBuffer) : LoginType() + class QRCode(val qrCodeLoginData: QRCodeLoginData) : LoginType() + } + private inner class FastLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) { override suspend fun doLogin() { val login10 = handler.sendAndExpect(WtLogin10(client)) diff --git a/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt b/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt index 4d5dfbb88..40cda87d8 100644 --- a/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt +++ b/mirai-core/src/commonMain/kotlin/network/handler/CommonNetworkHandler.kt @@ -17,6 +17,7 @@ import net.mamoe.mirai.internal.network.components.* import net.mamoe.mirai.internal.network.handler.NetworkHandler.Companion.runUnwrapCancellationException import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector +import net.mamoe.mirai.internal.network.handler.selector.SelectorRequireReconnectException import net.mamoe.mirai.internal.network.handler.state.StateObserver import net.mamoe.mirai.internal.network.impl.HeartbeatFailedException import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket @@ -253,7 +254,13 @@ internal abstract class CommonNetworkHandler( this@CommonNetworkHandler.launch { resumeConnection() } // go to next state. } else { // failed in SSO stage - context[SsoProcessor].casFirstLoginResult(null, FirstLoginResult.OTHER_FAILURE) + context[SsoProcessor].casFirstLoginResult( + null, + when (error) { + is SelectorRequireReconnectException -> null + else -> FirstLoginResult.OTHER_FAILURE + } + ) if (error is CancellationException) { // CancellationException is either caused by parent cancellation or manual `connectResult.cancel`. diff --git a/mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt b/mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt new file mode 100644 index 000000000..a06fb3263 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt @@ -0,0 +1,15 @@ +/* + * 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.handler.selector + +/** + * A special exception that instructs selector to restart network connection + */ +internal expect class SelectorRequireReconnectException() : NetworkException diff --git a/mirai-core/src/commonMain/kotlin/network/keys.kt b/mirai-core/src/commonMain/kotlin/network/keys.kt index d6077970e..ff57c79f4 100644 --- a/mirai-core/src/commonMain/kotlin/network/keys.kt +++ b/mirai-core/src/commonMain/kotlin/network/keys.kt @@ -74,6 +74,17 @@ internal fun BytePacketBuilder.writeLoginExtraData(loginExtraData: LoginExtraDat } } +@Serializable +internal class QRCodeLoginData( + val tmpPwd: ByteArray, // get from wtlogin.trans_emp, don't use client.wLoginSigInfo.encryptA1 + val noPicSig: ByteArray, // get from wtlogin.trans_emp, don't use client.wLoginSigInfo.encryptA1 + val tgtQR: ByteArray, +) { + override fun toString(): String { + return "QRCodeLoginData(tmpPwd=${tmpPwd.toUHexString()}, noPicSig=${noPicSig.toUHexString()}, tgtQR=${tgtQR.toUHexString()})" + } +} + @Suppress("ArrayInDataClass") // for `copy` @Serializable internal data class WLoginSigInfo( diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt index 54089e1e6..58408640c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/OutgoingPacket.kt @@ -172,6 +172,7 @@ internal val NO_ENCRYPT: ByteArray = ByteArray(0) internal inline fun OutgoingPacketFactory.buildLoginOutgoingPacket( client: QQAndroidClient, bodyType: Byte, + uin: String = client.uin.toString(), extraData: ByteArray = EMPTY_BYTE_ARRAY, remark: String? = null, commandName: String = this.commandName, @@ -190,7 +191,7 @@ internal inline fun OutgoingPacketFactory.buildLoginOutgoingPac } writeByte(0x00) - client.uin.toString().let { + uin.let { writeInt(it.length + 4) writeText(it) } @@ -274,6 +275,7 @@ internal inline fun BytePacketBuilder.writeSsoPacket( internal fun BytePacketBuilder.writeOicqRequestPacket( client: QQAndroidClient, + uin: Long = client.uin, encryptMethod: EncryptMethod = EncryptMethodEcdh(client.bot.components[EcdhInitialPublicKeyUpdater].getQQEcdh()), commandId: Int, bodyBlock: BytePacketBuilder.() -> Unit @@ -284,7 +286,7 @@ internal fun BytePacketBuilder.writeOicqRequestPacket( writeShort(8001) writeShort(commandId.toShort()) writeShort(1) // const?? - writeInt(client.uin.toInt()) + writeInt(uin.toInt()) writeByte(3) // originally const writeByte(encryptMethod.id.toByte()) writeByte(0) // const8_always_0 diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt index 8e7784eb4..2c6ab32e0 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt @@ -131,6 +131,7 @@ internal object KnownPacketFactories { object OutgoingFactories : List> by mutableListOf( WtLogin.Login, WtLogin.ExchangeEmp, + WtLogin.TransEmp, StatSvc.Register, StatSvc.GetOnlineStatus, StatSvc.SimpleGet, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt index 4ce77c137..543862e0a 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/Tlv.kt @@ -82,6 +82,26 @@ internal fun BytePacketBuilder.t8( } } +internal fun BytePacketBuilder.t16( + ssoVersion: Int, + subAppId: Long, + guid: ByteArray, + apkId: ByteArray, + apkVersionName: ByteArray, + apkSignatureMd5: ByteArray +) { + writeShort(0x16) + writeShortLVPacket { + writeInt(ssoVersion) + writeInt(16) + writeInt(subAppId.toInt()) + writeFully(guid) + writeShortLVByteArray(apkId) + writeShortLVByteArray(apkVersionName) + writeShortLVByteArray(apkSignatureMd5) + } +} + internal fun BytePacketBuilder.t18( appId: Long, appClientVersion: Int = 0, @@ -100,9 +120,81 @@ internal fun BytePacketBuilder.t18( } shouldEqualsTo 22 } +internal fun BytePacketBuilder.t1b( + micro: Int = 0, + version: Int = 0, + size: Int = 3, + margin: Int = 4, + dpi: Int = 72, + ecLevel: Int = 2, + hint: Int = 2 +) { + writeShort(0x1b) + writeShortLVPacket { + writeInt(micro) + writeInt(version) + writeInt(size) + writeInt(margin) + writeInt(dpi) + writeInt(ecLevel) + writeInt(hint) + writeShort(0) + } +} + +internal fun BytePacketBuilder.t1d( + miscBitmap: Int, +) { + writeShort(0x1d) + writeShortLVPacket { + writeByte(1) + writeInt(miscBitmap) + writeInt(0) + writeByte(0) + writeInt(0) + } +} + +internal fun BytePacketBuilder.t1f( + isRoot: Boolean = false, + osName: ByteArray, + osVersion: ByteArray, + simVendor: ByteArray, + apn: ByteArray, + networkType: Short = 2, +) { + writeShort(0x1f) + writeShortLVPacket { + writeByte(if (isRoot) 1 else 0) + writeShortLVByteArray(osName) + writeShortLVByteArray(osVersion) + writeShort(networkType) + writeShortLVByteArray(simVendor) + writeShortLVByteArray(EMPTY_BYTE_ARRAY) + writeShortLVByteArray(apn) + } +} + +internal fun BytePacketBuilder.t33( + guid: ByteArray, +) { + writeShort(0x33) + writeShortLVByteArray(guid) +} + +internal fun BytePacketBuilder.t35( + productType: Int +) { + writeShort(0x35) + writeShortLVPacket { + writeInt(productType) + } +} + internal fun BytePacketBuilder.t106( + client: QQAndroidClient, appId: Long = 16L, - client: QQAndroidClient + passwordMd5: ByteArray, ) { return t106( appId, @@ -110,7 +202,7 @@ internal fun BytePacketBuilder.t106( client.appClientVersion, client.uin, true, - client.account.passwordMd5, + passwordMd5, 0, client.uin.toByteArray(), client.tgtgtKey, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt index bbc16a729..ec4310549 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/WtLogin.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -25,7 +25,10 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLoginExt import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.analysisTlv0x531 import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.orEmpty import net.mamoe.mirai.internal.utils.crypto.TEA +import net.mamoe.mirai.internal.utils.io.writeShortLVByteArray +import net.mamoe.mirai.internal.utils.io.writeShortLVPacket import net.mamoe.mirai.internal.utils.printStructure +import net.mamoe.mirai.network.InconsistentBotIdException import net.mamoe.mirai.network.RetryLaterException import net.mamoe.mirai.network.WrongPasswordException import net.mamoe.mirai.utils.* @@ -130,7 +133,8 @@ internal class WtLogin { t142(client.apkId) t145(client.device.guid) t154(0) - t112(client.account.phoneNumber.encodeToByteArray()) + // 需要 t112, 但在实现 QR 时删除了 phoneNumber +// t112(client.account.phoneNumber.encodeToByteArray()) t116(client.miscBitMap, client.subSigMap) t521() t52c() @@ -635,7 +639,7 @@ internal class WtLogin { deviceToken = tlvMap119.getOrEmpty(0x322), encryptedDownloadSession = tlvMap119[0x11d]?.let { client.analysisTlv11d(it) - } + }, ) } //bot.network.logger.error(client.wLoginSigInfo.psKeyMap["qun.qq.com"]?.data?.encodeToString()) @@ -655,4 +659,240 @@ internal class WtLogin { } } + + internal object TransEmp : OutgoingPacketFactory("wtlogin.trans_emp") { + + fun FetchQRCode( + client: QQAndroidClient, + size: Int, + margin: Int, + ecLevel: Int + ) = TransEmp.buildLoginOutgoingPacket(client, bodyType = 2, uin = "") { sequenceId -> + writeSsoPacket(client, client.subAppId, TransEmp.commandName, sequenceId = sequenceId) { + writeOicqRequestPacket(client, uin = 0, commandId = 0x812) { + val code2dPacket = buildCode2dPacket(0, 0, 0x31) { + writeShort(0) + writeInt(16) + writeLong(0) + writeByte(8) + writeShortLVPacket { } + + writeShort(6) + t16( + client.ssoVersion, + client.subAppId, + client.device.guid, + client.apkId, + client.apkVersionName, + client.apkSignatureMd5 + ) + t1b( + size = size, + margin = margin, + ecLevel = ecLevel + ) + t1d(client.miscBitMap) + + val protocol = client.bot.configuration.protocol + when (protocol) { + BotConfiguration.MiraiProtocol.MACOS -> t1f( + false, + "Mac OSX".toByteArray(), + "10".toByteArray(), + "mac carrier".toByteArray(), + client.device.apn, + 2 + ) + + BotConfiguration.MiraiProtocol.ANDROID_WATCH -> t1f( + false, + client.device.osType, + "7.1.2".toByteArray(), + "China Mobile GSM".toByteArray(), + client.device.apn, + 2 + ) + + else -> error("protocol $protocol doesn't support qrcode login.") + } + + t33(client.device.guid) + t35( + when (protocol) { + BotConfiguration.MiraiProtocol.MACOS -> 5 + BotConfiguration.MiraiProtocol.ANDROID_WATCH -> 8 + else -> error("assertion") + } + ) + } + writeByte(0) + writeShort(code2dPacket.remaining.toShort()) + writeInt(0x10) // appId, const 16 + writeInt(0x72) // 0x90 + writeFully(ByteArray(3) { 0x00 }) + writePacket(code2dPacket) + code2dPacket.release() + } + } + } + + fun QueryQRCodeStatus( + client: QQAndroidClient, + sig: ByteArray, + ) = TransEmp.buildLoginOutgoingPacket(client, bodyType = 2, uin = "") { sequenceId -> + writeSsoPacket(client, client.subAppId, TransEmp.commandName, sequenceId = sequenceId) { + writeOicqRequestPacket(client, uin = 0, commandId = 0x812) { + val code2dPacket = buildCode2dPacket(1, 0, 0x12) { + writeShort(5) + writeByte(1) + writeInt(8) + writeInt(16) + writeShortLVByteArray(sig) + writeLong(0) + writeByte(8) + writeShortLVPacket { } + writeShort(0) + } + writeByte(0) + writeShort(code2dPacket.remaining.toShort()) + writeInt(0x10) // appId, const 16 + writeInt(0x72) // 0x90 + writeFully(ByteArray(3) { 0x00 }) + writePacket(code2dPacket) + code2dPacket.release() + } + } + } + + private fun buildCode2dPacket( + sequence: Int, uin: Long, command: Short, body: BytePacketBuilder.() -> Unit + ) = buildPacket { + writeInt(currentTimeSeconds().toInt()) + writeByte(2) + val bodyPacket = buildPacket(body) + writeUShort((43 + bodyPacket.remaining + 1).toUShort()) + writeUShort(command.toUShort()) + writeFully(ByteArray(21) { 0 }) + writeByte(3) + writeShort(0) + writeShort(50) + writeInt(sequence) + writeLong(uin) + writePacket(bodyPacket) + bodyPacket.release() + writeByte(3) + } + + sealed class Response() : Packet { + class FetchQRCode(val imageData: ByteArray, val sig: ByteArray) : Response() { + override fun toString(): String { + return "WtLogin.TransEmp.Response.FetchQRCode" + + "(imageData=${imageData.toUHexString()}, sig=${sig.toUHexString()})" + } + } + + class QRCodeStatus(val state: State) : Response() { + override fun toString(): String { + return "WtLogin.TransEmp.Response.QRCodeStatus(state=$state)" + } + + enum class State { WAITING_FOR_SCAN, WAITING_FOR_CONFIRM, CANCELLED, TIMEOUT } + } + + class QRCodeConfirmed(val data: QRCodeLoginData) : Response() { + override fun toString(): String { + return "WtLogin.TransEmp.Response.QRCodeConfirmed(data=$data)" + } + } + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { + check(remaining >= 48) { "remaining payload is too short, current is $remaining." } + + discardExact(5) + readUByte() + readUShort() + val command = readUShort().toInt() + discardExact(21) + readUByte() + readUShort() + readUShort() + readInt() + readLong() + + return when (command) { + 0x31 -> { // qr code data + readShort() + readInt() + + val code = readByte().toInt() + check(code == 0) { "code is not 0 while parsing wtlogin.trans_emp with command 0x31." } + val sig = readUShortLVByteArray() + readUShort() + + val tlv = _readTLVMap() + val data = + tlv.getOrFail(0x17) { "missing tlv 0x17 while parsing wtlogin.trans_emp with command 0x31." } + + Response.FetchQRCode(data, sig) + } + + 0x12 -> { // qr code state + var length = readUShort().toInt() + if (length != 0) { + length-- + if (readUByte().toInt() == 2) { + readLong() + length -= 8 + } + } + + if (length > 0) { + discardExact(length) + } + readInt() + + val code = readUByte().toInt() + if (code != 0) { + when (code) { // code + 0x30 -> Response.QRCodeStatus(Response.QRCodeStatus.State.WAITING_FOR_SCAN) + 0x35 -> Response.QRCodeStatus(Response.QRCodeStatus.State.WAITING_FOR_CONFIRM) + 0x36 -> Response.QRCodeStatus(Response.QRCodeStatus.State.CANCELLED) + 0x11 -> Response.QRCodeStatus(Response.QRCodeStatus.State.TIMEOUT) + else -> error("unknown code $code while parsing wtlogin.trans_emp with command 0x12.") + } + } else { + val client = bot.client + + val uin = readLong() + if (client.uin != uin) { + throw InconsistentBotIdException(expected = client.uin, actual = uin) + } + readInt() + readUShort() + val tlv = _readTLVMap() + + val tmpPwd = tlv.getOrFail(0x18) { + "missing tlv 0x18 while parsing wtlogin.trans_emp with command 0x12." + } + val noPicSig = tlv.getOrFail(0x19) { + "missing tlv 0x19 while parsing wtlogin.trans_emp with command 0x12." + } + val tgtQR = tlv.getOrFail(0x65) { + "missing tlv 0x65 while parsing wtlogin.trans_emp with command 0x12." + } + + client.tgtgtKey = tlv.getOrFail(0x1e) { + "missing tlv 0x1e while parsing wtlogin.trans_emp with command 0x12." + } + + Response.QRCodeConfirmed(QRCodeLoginData(tmpPwd, noPicSig, tgtQR)) + } + } + + else -> error("wtlogin.trans_emp received an unknown command: $command") + } + } + + } } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt index 316267776..f3ed8bfd7 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin15.kt @@ -29,11 +29,11 @@ internal object WtLogin15 : WtLoginExt { // writeSsoPacket(client, client.subAppId, WtLogin.ExchangeEmp.commandName, sequenceId = sequenceId) { writeOicqRequestPacket( client, - EncryptMethodSessionKeyNew( + encryptMethod = EncryptMethodSessionKeyNew( client.wLoginSigInfo.wtSessionTicket.data, client.wLoginSigInfo.wtSessionTicketKey ), - 0x0810 + commandId = 0x0810 ) { writeShort(subCommand) // subCommand writeShort(24) diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt index a23e4fd06..9f4cbb94c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/login/wtlogin/WtLogin9.kt @@ -17,8 +17,9 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin internal object WtLogin9 : WtLoginExt { private const val appId = 16L - operator fun invoke( + fun Password( client: QQAndroidClient, + passwordMd5: ByteArray, allowSlider: Boolean ) = WtLogin.Login.buildLoginOutgoingPacket( client, bodyType = 2, remark = "9:password-login" @@ -43,7 +44,7 @@ internal object WtLogin9 : WtLoginExt { if (useEncryptA1AndNoPicSig) { t106(client.wLoginSigInfo.encryptA1!!) } else { - t106(appId, client) + t106(client, appId, passwordMd5) } /* // from GetStWithPasswd @@ -118,4 +119,56 @@ internal object WtLogin9 : WtLoginExt { } } } + + @Suppress("DuplicatedCode") + fun QRCode( + client: QQAndroidClient, + data: QRCodeLoginData, + ) = WtLogin.Login.buildLoginOutgoingPacket( + client, bodyType = 2, remark = "9:qrcode-login" + ) { sequenceId -> + writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) { + writeOicqRequestPacket(client, commandId = 0x0810) { + writeShort(9) // subCommand + writeShort(0x19) // count of TLVs, probably ignored by server? + + t18(appId, client.appClientVersion, client.uin) + t1(client.uin, client.device.ipAddress) + + t106(data.tmpPwd) + + t116(client.miscBitMap, client.subSigMap) + t100(appId, client.subAppId, client.appClientVersion, client.ssoVersion, client.mainSigMap) + t107(0) + t108(client.device.imei.toByteArray()) + + t142(client.apkId) + + t144(client) + + t145(client.device.guid) + t147(appId, client.apkVersionName, client.apkSignatureMd5) + + t16a(data.noPicSig) + + t154(sequenceId) + t141(client.device.simInfo, client.networkType, client.device.apn) + t8(2052) + + t511() + + t187(client.device.macAddress) + t188(client.device.androidId) + t194(client.device.imsiMd5) + t191(0x00) + + t202(client.device.wifiBSSID, client.device.wifiSSID) + + t177(client.buildTime, client.sdkVersion) + t516() + t521(8) + t318(data.tgtQR) + } + } + } } diff --git a/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt b/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt index 0535107a9..e6512e296 100644 --- a/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt +++ b/mirai-core/src/commonMain/kotlin/utils/MiraiCoreServices.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -115,5 +115,10 @@ internal object MiraiCoreServices { "net.mamoe.mirai.message.data.OfflineAudio.Factory", "net.mamoe.mirai.internal.message.data.OfflineAudioFactoryImpl" ) { net.mamoe.mirai.internal.message.data.OfflineAudioFactoryImpl() } + + Services.register( + "net.mamoe.mirai.auth.DefaultBotAuthorizationFactory", + "net.mamoe.mirai.internal.network.auth.DefaultBotAuthorizationFactoryImpl" + ) { net.mamoe.mirai.internal.network.auth.DefaultBotAuthorizationFactoryImpl() } } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt b/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt index fb0824d0d..3153d0bb2 100644 --- a/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt +++ b/mirai-core/src/commonMain/kotlin/utils/MiraiProtocolInternal.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -25,6 +25,7 @@ internal class MiraiProtocolInternal( @JvmField internal val sign: String, @JvmField internal val buildTime: Long, @JvmField internal val ssoVersion: Int, + @JvmField internal val supportsQRLogin: Boolean, ) { internal companion object { internal val protocols = EnumMap(MiraiProtocol::class) @@ -35,67 +36,73 @@ internal class MiraiProtocolInternal( init { //Updated from MiraiGo (2023/3/7) protocols[MiraiProtocol.ANDROID_PHONE] = MiraiProtocolInternal( - "com.tencent.mobileqq", - 537151682, - "8.9.33.10335", - "6.0.0.2534", - 150470524, - 0x10400, - 16724722, - "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", - 1673599898L, - 19, + apkId = "com.tencent.mobileqq", + id = 537151682, + ver = "8.9.33.10335", + sdkVer = "6.0.0.2534", + miscBitMap = 150470524, + subSigMap = 0x10400, + mainSigMap = 16724722, + sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", + buildTime = 1673599898L, + ssoVersion = 19, + supportsQRLogin = false, ) //Updated from MiraiGo (2023/3/7) protocols[MiraiProtocol.ANDROID_PAD] = MiraiProtocolInternal( - "com.tencent.mobileqq", - 537151218, - "8.9.33.10335", - "6.0.0.2534", - 150470524, - 0x10400, - 16724722, - "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", - 1673599898L, - 19, + apkId = "com.tencent.mobileqq", + id = 537151218, + ver = "8.9.33.10335", + sdkVer = "6.0.0.2534", + miscBitMap = 150470524, + subSigMap = 0x10400, + mainSigMap = 16724722, + sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", + buildTime = 1673599898L, + ssoVersion = 19, + supportsQRLogin = false, ) protocols[MiraiProtocol.ANDROID_WATCH] = MiraiProtocolInternal( - "com.tencent.qqlite", - 537064446, - "2.0.5", - "6.0.0.236", - 16252796, - 0x10400, - 34869472, - "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", - 1559564731L, - 5, + apkId = "com.tencent.qqlite", + id = 537064446, + ver = "2.0.5", + sdkVer = "6.0.0.236", + miscBitMap = 16252796, + subSigMap = 0x10400, + mainSigMap = 34869472, + sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D", + buildTime = 1559564731L, + ssoVersion = 5, + supportsQRLogin = true, ) protocols[MiraiProtocol.IPAD] = MiraiProtocolInternal( - "com.tencent.minihd.qq", - 537151363, - "8.9.33.614", - "6.0.0.2433", - 150470524, - 66560, - 1970400, - "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7", - 1640921786L, - 12, + apkId = "com.tencent.minihd.qq", + id = 537151363, + ver = "8.9.33.614", + sdkVer = "6.0.0.2433", + miscBitMap = 150470524, + subSigMap = 66560, + mainSigMap = 1970400, + sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7", + buildTime = 1640921786L, + ssoVersion = 12, + supportsQRLogin = false, ) - //Updated from MiraiGo (2023/3/15) protocols[MiraiProtocol.MACOS] = MiraiProtocolInternal( - "com.tencent.qq", - 537128930, - "5.8.9", - "6.0.0.2433", - 150470524, - 66560, - 1970400, - "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7", - 1595836208L, - 12, + apkId = "com.tencent.qq", + id = 0x2003ca32, + ver = "6.7.9", + sdkVer = "6.2.0.1023", + miscBitMap = 0x7ffc, + subSigMap = 66560, + mainSigMap = 1970400, + sign = "com.tencent.qq".encodeToByteArray().toUHexString(" "), + buildTime = 0L, + ssoVersion = 7, + supportsQRLogin = true, ) } + + inline val MiraiProtocol.asInternal: MiraiProtocolInternal get() = get(this) } } diff --git a/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.auth.DefaultBotAuthorizationFactory b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.auth.DefaultBotAuthorizationFactory new file mode 100644 index 000000000..644d9ed5a --- /dev/null +++ b/mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.auth.DefaultBotAuthorizationFactory @@ -0,0 +1,10 @@ +# +# 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 +# + +net.mamoe.mirai.internal.network.auth.DefaultBotAuthorizationFactoryImpl \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/component/BotAuthControlTest.kt b/mirai-core/src/commonTest/kotlin/network/component/BotAuthControlTest.kt new file mode 100644 index 000000000..4ed2708ae --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/network/component/BotAuthControlTest.kt @@ -0,0 +1,108 @@ +/* + * 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.component + +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield +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.auth.AuthControl +import net.mamoe.mirai.internal.network.components.SsoProcessorContext +import net.mamoe.mirai.internal.network.components.SsoProcessorImpl +import net.mamoe.mirai.internal.network.framework.AbstractCommonNHTest +import net.mamoe.mirai.network.CustomLoginFailedException +import net.mamoe.mirai.utils.BotConfiguration +import net.mamoe.mirai.utils.DeviceInfo +import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY +import kotlin.reflect.KClass +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.fail + +internal class BotAuthControlTest : AbstractCommonNHTest() { + private val botAuthInfo = object : BotAuthInfo { + override val id: Long + get() = bot.id + override val deviceInfo: DeviceInfo + get() = bot.components[SsoProcessorContext].device + override val configuration: BotConfiguration + get() = bot.configuration + } + + private suspend fun AuthControl.assertRequire(exceptedType: KClass<*>) { + println("Requiring auth method") + val nextAuth = acquireAuth() + println("Got $nextAuth") + yield() + + if (nextAuth is SsoProcessorImpl.AuthMethod.Error) { + fail(cause = nextAuth.exception) + } + if (exceptedType.isInstance(nextAuth)) return + fail("Type not match, excepted $exceptedType but got ${nextAuth::class}") + } + + @Test + fun `auth test`() = runTest { + + val control = AuthControl(botAuthInfo, object : BotAuthorization { + override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult { + return session.authByPassword(EMPTY_BYTE_ARRAY) + } + }, bot.logger, backgroundScope.coroutineContext) + + control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class) + control.actComplete() + control.assertRequire(SsoProcessorImpl.AuthMethod.NotAvailable::class) + + } + + @Test + fun `test auth failed and reselect`() = runTest { + class MyLoginFailedException : CustomLoginFailedException(killBot = false) + + val control = AuthControl(botAuthInfo, object : BotAuthorization { + override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult { + assertFailsWith { session.authByPassword(EMPTY_BYTE_ARRAY); println("!") } + println("114514") + return session.authByPassword(EMPTY_BYTE_ARRAY) + } + }, bot.logger, backgroundScope.coroutineContext) + + control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class) + control.actMethodFailed(MyLoginFailedException()) + + control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class) + control.actComplete() + + control.assertRequire(SsoProcessorImpl.AuthMethod.NotAvailable::class) + + } + + @Test + fun `failed when login complete`() = runTest { + val control = AuthControl(botAuthInfo, object : BotAuthorization { + override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult { + val rsp = session.authByPassword(EMPTY_BYTE_ARRAY) + assertFailsWith { session.authByPassword(EMPTY_BYTE_ARRAY) } + assertFailsWith { session.authByPassword(EMPTY_BYTE_ARRAY) } + assertFailsWith { session.authByPassword(EMPTY_BYTE_ARRAY) } + return rsp + } + }, bot.logger, backgroundScope.coroutineContext) + + control.assertRequire(SsoProcessorImpl.AuthMethod.Pwd::class) + control.actComplete() + control.assertRequire(SsoProcessorImpl.AuthMethod.NotAvailable::class) + + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt index cbff752ed..18c6fbced 100644 --- a/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt +++ b/mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -13,12 +13,15 @@ package net.mamoe.mirai.internal.network.framework import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import net.mamoe.mirai.auth.BotAuthInfo +import net.mamoe.mirai.auth.BotAuthResult import net.mamoe.mirai.internal.* import net.mamoe.mirai.internal.contact.uin import net.mamoe.mirai.internal.network.KeyWithCreationTime import net.mamoe.mirai.internal.network.KeyWithExpiry import net.mamoe.mirai.internal.network.WLoginSigInfo import net.mamoe.mirai.internal.network.WLoginSimpleInfo +import net.mamoe.mirai.internal.network.auth.BotAuthSessionInternal import net.mamoe.mirai.internal.network.component.ComponentKey import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage import net.mamoe.mirai.internal.network.component.setAll @@ -113,6 +116,31 @@ internal abstract class AbstractRealNetworkHandlerTest : Abs set(SsoProcessorContext, SsoProcessorContextImpl(bot)) set(SsoProcessor, object : TestSsoProcessor(bot) { override suspend fun login(handler: NetworkHandler) { + val botAuthInfo = object : BotAuthInfo { + override val id: Long get() = bot.id + override val deviceInfo: DeviceInfo + get() = get(SsoProcessorContext).device + override val configuration: BotConfiguration + get() = bot.configuration + } + val rsp = object : BotAuthResult {} + + val session = object : BotAuthSessionInternal() { + override suspend fun authByPassword(passwordMd5: SecretsProtection.EscapedByteBuffer): BotAuthResult { + return rsp + } + + override suspend fun authByQRCode(): BotAuthResult { + return rsp + } + + } + + bot.account.authorization.authorize(session, botAuthInfo) + bot.account.accountSecretsKeyBuffer = SecretsProtection.EscapedByteBuffer( + bot.account.authorization.calculateSecretsKey(botAuthInfo) + ) + nhEvents.add(NHEvent.Login) super.login(handler) } diff --git a/mirai-core/src/jvmBaseMain/kotlin/BotAccount.kt b/mirai-core/src/jvmBaseMain/kotlin/BotAccount.kt deleted file mode 100644 index 204a34966..000000000 --- a/mirai-core/src/jvmBaseMain/kotlin/BotAccount.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2019-2022 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 - -import net.mamoe.mirai.utils.* -import java.nio.ByteBuffer - -internal actual data class BotAccount( - internal actual val id: Long, - - val passwordMd5Buffer: ByteBuffer, // md5 - - actual val phoneNumber: String = "" -) { - init { - check(passwordMd5Buffer.remaining == 16) { - "Invalid passwordMd5: size must be 16 but got ${passwordMd5Buffer.remaining}. passwordMd5=${passwordMd5.toUHexString()}" - } - } - - actual constructor(id: Long, passwordMd5: ByteArray, phoneNumber: String) : this( - id, SecretsProtection.escape(passwordMd5), phoneNumber - ) - - actual constructor(id: Long, passwordPlainText: String, phoneNumber: String) : this( - id, - passwordPlainText.md5(), - phoneNumber - ) { - require(passwordPlainText.length <= 16) { "Password length must be at most 16." } - } - - actual val passwordMd5: ByteArray - get() { - return passwordMd5Buffer.duplicate().readBytes() - } - - actual override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as BotAccount - - if (id != other.id) return false - if (passwordMd5Buffer != other.passwordMd5Buffer) return false - - return true - } - - - actual override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + passwordMd5Buffer.hashCode() - return result - } -} \ No newline at end of file diff --git a/mirai-core/src/jvmBaseMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt b/mirai-core/src/jvmBaseMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt new file mode 100644 index 000000000..dab4f0b0d --- /dev/null +++ b/mirai-core/src/jvmBaseMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt @@ -0,0 +1,29 @@ +/* + * 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.handler.selector + +internal actual class SelectorRequireReconnectException( + withStackTrace: Boolean +) : NetworkException(true) { + actual constructor() : this(false) + + private companion object { + val EMPTY = arrayOf() + } + + override fun fillInStackTrace(): Throwable { + stackTrace = EMPTY + return this + } + + init { + if (withStackTrace) super.fillInStackTrace() + } +} \ No newline at end of file diff --git a/mirai-core/src/jvmTest/kotlin/directboot/DebugRunHelper.kt b/mirai-core/src/jvmTest/kotlin/directboot/DebugRunHelper.kt index 85ad194c3..1fd5af9c2 100644 --- a/mirai-core/src/jvmTest/kotlin/directboot/DebugRunHelper.kt +++ b/mirai-core/src/jvmTest/kotlin/directboot/DebugRunHelper.kt @@ -11,13 +11,18 @@ package net.mamoe.mirai.internal.directboot import kotlinx.coroutines.Dispatchers import net.mamoe.mirai.BotFactory +import net.mamoe.mirai.auth.BotAuthorization import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.utils.BotConfiguration import java.io.File internal object DebugRunHelper { fun newBot(id: Long, pwd: String, conf: BotConfiguration.(botid: Long) -> Unit): QQAndroidBot { - val bot = BotFactory.newBot(id, pwd) { + return newBot(id, BotAuthorization.byPassword(pwd), conf) + } + + fun newBot(id: Long, authorization: BotAuthorization, conf: BotConfiguration.(botid: Long) -> Unit): QQAndroidBot { + val bot = BotFactory.newBot(id, authorization) { parentCoroutineContext = Dispatchers.IO workingDir = File("test/session/$id").also { it.mkdirs() }.absoluteFile diff --git a/mirai-core/src/nativeMain/kotlin/BotAccount.kt b/mirai-core/src/nativeMain/kotlin/BotAccount.kt deleted file mode 100644 index 3f4cd9791..000000000 --- a/mirai-core/src/nativeMain/kotlin/BotAccount.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019-2022 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 - -import net.mamoe.mirai.utils.isSameClass -import net.mamoe.mirai.utils.md5 - -internal actual class BotAccount actual constructor( - internal actual val id: Long, - actual val passwordMd5: ByteArray, - actual val phoneNumber: String, -) { - actual constructor(id: Long, passwordPlainText: String, phoneNumber: String) : this( - id, - passwordPlainText.md5(), - phoneNumber - ) - - actual override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is BotAccount || !isSameClass(this, other)) return false - - if (id != other.id) return false - if (!passwordMd5.contentEquals(other.passwordMd5)) return false - - return true - } - - actual override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + passwordMd5.hashCode() - return result - } -} \ No newline at end of file diff --git a/mirai-core/src/nativeMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt b/mirai-core/src/nativeMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt new file mode 100644 index 000000000..040bd49dc --- /dev/null +++ b/mirai-core/src/nativeMain/kotlin/network/handler/selector/SelectorRequireReconnectException.kt @@ -0,0 +1,12 @@ +/* + * 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.handler.selector + +internal actual class SelectorRequireReconnectException : NetworkException(true)