mirror of
https://github.com/mamoe/mirai.git
synced 2026-08-22 06:03:32 +08:00
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.<init>` 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 c1136a8798.
---------
Co-authored-by: Karlatemp <kar@kasukusakura.com>
Co-authored-by: Him188 <Him188@mamoe.net>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<QQAndroidBot> { 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<QQAndroidBot> { 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 <T> awaitDeferred(
|
||||
@BuilderInference
|
||||
crossinline block: suspend (CompletableDeferred<T>) -> Unit
|
||||
): T {
|
||||
val deferred = CompletableDeferred<T>()
|
||||
block(deferred)
|
||||
return deferred.await()
|
||||
}
|
||||
Reference in New Issue
Block a user