mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-26 20:20:14 +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:
parent
e5ff458a5d
commit
78d0b4fd54
@ -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()
|
||||
}
|
@ -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 <init> (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 <init> (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public synthetic fun <init> (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 <init> (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 <init> (Z)V
|
||||
public fun <init> (ZLjava/lang/String;)V
|
||||
public fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;)V
|
||||
public fun <init> (ZLjava/lang/Throwable;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
|
||||
public fun <init> (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 <init> (Ljava/lang/String;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
|
||||
public fun <init> (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 <init> ()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;
|
||||
|
@ -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 <init> (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 <init> (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public synthetic fun <init> (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 <init> (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 <init> (Z)V
|
||||
public fun <init> (ZLjava/lang/String;)V
|
||||
public fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;)V
|
||||
public fun <init> (ZLjava/lang/Throwable;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
|
||||
public fun <init> (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 <init> (Ljava/lang/String;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
|
||||
public fun <init> (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 <init> ()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 <init> (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;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
128
mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt
Normal file
128
mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt
Normal file
@ -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
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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 实现的异常
|
||||
|
@ -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`.")
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滑动验证码.
|
||||
*
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
91
mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt
Normal file
91
mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt
Normal file
@ -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<EscapedString> by SecretsProtectionPlatform.EscapedStringSerializer
|
||||
|
||||
public object EscapedByteBufferSerializer :
|
||||
KSerializer<EscapedByteBuffer> 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<SecretsProtection.EscapedString>
|
||||
|
||||
object EscapedByteBufferSerializer : KSerializer<SecretsProtection.EscapedByteBuffer>
|
||||
}
|
@ -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,38 +147,39 @@ 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<EscapedString> by String.serializer().map(
|
||||
actual fun impl_getSize(data: Any): Int {
|
||||
return (data as ByteBuffer).remaining
|
||||
}
|
||||
|
||||
actual object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString> 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<ByteBuffer>().duplicate().readString() }
|
||||
)
|
||||
|
||||
public object EscapedByteBufferSerializer : KSerializer<EscapedByteBuffer> by ByteArraySerializer().map(
|
||||
actual object EscapedByteBufferSerializer :
|
||||
KSerializer<SecretsProtection.EscapedByteBuffer> by ByteArraySerializer().map(
|
||||
ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
|
||||
deserialize = { EscapedByteBuffer(escape(it)) },
|
||||
serialize = { it.data.duplicate().readBytes() }
|
||||
deserialize = { SecretsProtection.EscapedByteBuffer(escape(it)) },
|
||||
serialize = { it.data.cast<ByteBuffer>().duplicate().readBytes() }
|
||||
)
|
||||
|
||||
|
||||
|
@ -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()
|
||||
)
|
||||
|
47
mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt
Normal file
47
mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt
Normal file
@ -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<ByteArray>().size
|
||||
}
|
||||
|
||||
actual fun escape(data: ByteArray): Any {
|
||||
return data
|
||||
}
|
||||
|
||||
actual object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString> by String.serializer().map(
|
||||
String.serializer().descriptor.copy("EscapedString"),
|
||||
deserialize = { SecretsProtection.EscapedString(it.encodeToByteArray()) },
|
||||
serialize = { it.data.cast<ByteArray>().decodeToString() }
|
||||
)
|
||||
|
||||
actual object EscapedByteBufferSerializer :
|
||||
KSerializer<SecretsProtection.EscapedByteBuffer> by ByteArraySerializer().map(
|
||||
ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
|
||||
deserialize = { SecretsProtection.EscapedByteBuffer(it) },
|
||||
serialize = { it.data.cast() }
|
||||
)
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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
|
||||
|
116
mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt
Normal file
116
mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt
Normal file
@ -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<Throwable?, SsoProcessorImpl.AuthMethod> =
|
||||
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 <R> 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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<T, V>(
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
private val logger: MiraiLogger,
|
||||
private val producerCoroutine: suspend OnDemandProducerScope<T, V>.(initialTicket: T) -> Unit,
|
||||
) : OnDemandConsumer<T, V> {
|
||||
private val coroutineScope = parentCoroutineContext.childScope("CoroutineOnDemandValueScope")
|
||||
|
||||
private val state: AtomicRef<ProducerState<T, V>> = atomic(ProducerState.JustInitialized())
|
||||
|
||||
|
||||
inner class Producer(
|
||||
private val initialTicket: T,
|
||||
) : OnDemandProducerScope<T, V> {
|
||||
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<T, V>, newState: ProducerState<T, V>): 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<V>(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)
|
||||
}
|
||||
}
|
@ -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(<ERASED>)"
|
||||
}
|
||||
}
|
||||
|
||||
override fun byQRCode(): BotAuthorization {
|
||||
return object : BotAuthorization {
|
||||
override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult =
|
||||
session.authByQRCode()
|
||||
|
||||
override fun toString(): String = "BotAuthorization.byQRCode()"
|
||||
}
|
||||
}
|
||||
}
|
51
mirai-core/src/commonMain/kotlin/network/auth/Latch.kt
Normal file
51
mirai-core/src/commonMain/kotlin/network/auth/Latch.kt
Normal file
@ -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<T> {
|
||||
/**
|
||||
* 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<T>)
|
||||
}
|
||||
|
||||
|
||||
internal fun <T> Latch(parentCoroutineContext: CoroutineContext): Latch<T> = LatchImpl(parentCoroutineContext)
|
||||
|
||||
private class LatchImpl<T>(
|
||||
parentCoroutineContext: CoroutineContext
|
||||
) : Latch<T> {
|
||||
private val deferred: CompletableDeferred<T> = CompletableDeferred(parentCoroutineContext[Job])
|
||||
|
||||
|
||||
override suspend fun acquire(): T = deferred.await()
|
||||
|
||||
override fun resumeWith(result: Result<T>) {
|
||||
if (!deferred.completeWith(result)) {
|
||||
error("$this was already resumed")
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "LatchImpl($deferred)"
|
||||
}
|
@ -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<T, V> {
|
||||
/**
|
||||
* 挂起协程, 直到 [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<T, V> {
|
||||
/**
|
||||
* 挂起协程并等待从 [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()
|
176
mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt
Normal file
176
mirai-core/src/commonMain/kotlin/network/auth/ProducerState.kt
Normal file
@ -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<T, V> {
|
||||
/*
|
||||
* 可变更状态的函数: [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<T, V> : ProducerState<T, V> {
|
||||
override fun toString(): String = "JustInitialized"
|
||||
}
|
||||
|
||||
sealed interface HasProducer<T, V> : ProducerState<T, V> {
|
||||
val producer: OnDemandProducerScope<T, V>
|
||||
}
|
||||
|
||||
// This is need — to ensure [launchProducer] is called exactly once.
|
||||
class CreatingProducer<T, V>(
|
||||
launchProducer: () -> OnDemandProducerScope<T, V>
|
||||
) : HasProducer<T, V> {
|
||||
override val producer: OnDemandProducerScope<T, V> by lazy(launchProducer)
|
||||
override fun toString(): String = "CreatingProducer"
|
||||
}
|
||||
|
||||
class ProducerReady<T, V>(
|
||||
override val producer: OnDemandProducerScope<T, V>,
|
||||
) : HasProducer<T, V> {
|
||||
override fun toString(): String = "ProducerReady"
|
||||
}
|
||||
|
||||
class Producing<T, V>(
|
||||
override val producer: OnDemandProducerScope<T, V>,
|
||||
val deferred: CompletableDeferred<V>,
|
||||
) : HasProducer<T, V> {
|
||||
override fun toString(): String = "Producing(deferred.completed=${deferred.isCompleted})"
|
||||
}
|
||||
|
||||
class Consuming<T, V>(
|
||||
override val producer: OnDemandProducerScope<T, V>,
|
||||
val value: Deferred<V>,
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
) : HasProducer<T, V> {
|
||||
val producerLatch = Latch<T>(parentCoroutineContext)
|
||||
|
||||
override fun toString(): String {
|
||||
val completed =
|
||||
value.runCatching { getCompleted().toString() }.getOrNull() // getCompleted() is experimental
|
||||
return "Consuming(value=$completed)"
|
||||
}
|
||||
}
|
||||
|
||||
class Consumed<T, V>(
|
||||
override val producer: OnDemandProducerScope<T, V>,
|
||||
val producerLatch: Latch<T>
|
||||
) : HasProducer<T, V> {
|
||||
override fun toString(): String = "Consumed($producerLatch)"
|
||||
}
|
||||
|
||||
class Finished<T, V>(
|
||||
val previousState: ProducerState<T, V>,
|
||||
val exception: Throwable?,
|
||||
) : ProducerState<T, V> {
|
||||
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)"
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ import net.mamoe.mirai.utils.lateinitMutableProperty
|
||||
internal interface BotClientHolder {
|
||||
var client: QQAndroidClient
|
||||
|
||||
fun refreshClient()
|
||||
|
||||
companion object : ComponentKey<BotClientHolder>
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
@ -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,43 +97,66 @@ 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) {
|
||||
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 flag2=$flag2", PROTOCOL_UPDATED)
|
||||
else -> throw PacketCodecException("Unknown encrypt type=$encryptMethod", PROTOCOL_UPDATED)
|
||||
}.let { decryptedData ->
|
||||
when (flag1) {
|
||||
when (type) {
|
||||
0x0A -> parseSsoFrame(client, decryptedData)
|
||||
0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样.
|
||||
else -> throw PacketCodecException(
|
||||
"unknown flag1: ${flag1.toByte().toUHexString()}",
|
||||
"unknown packet type: ${type.toByte().toUHexString()}",
|
||||
PROTOCOL_UPDATED
|
||||
)
|
||||
}
|
||||
}.let { raw ->
|
||||
when (flag2) {
|
||||
}
|
||||
} 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,
|
||||
@ -145,11 +169,11 @@ internal class PacketCodecImpl : PacketCodec {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
else -> error("unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DecodeResult constructor(
|
||||
val commandName: String,
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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<QRCodeLoginProcessor> {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
override suspend fun login(handler: NetworkHandler) {
|
||||
|
||||
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 {
|
||||
|
||||
// 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)
|
||||
|
||||
loginSuccess()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (authControl == null) initAuthControl()
|
||||
val authControl0 = authControl!!
|
||||
|
||||
|
||||
var nextAuthMethod: AuthMethod? = null
|
||||
try {
|
||||
ssoContext.bot.components[BotClientHolder].refreshClient()
|
||||
ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh()
|
||||
SlowLoginImpl(handler).doLogin()
|
||||
|
||||
when (val authw = authControl0.acquireAuth().also { nextAuthMethod = it }) {
|
||||
is AuthMethod.Error -> {
|
||||
authControl = null
|
||||
throw authw.exception
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Failed to log in, invalidate secrets.
|
||||
ssoContext.bot.components[AccountSecretsManager].invalidate()
|
||||
throw e
|
||||
|
||||
AuthMethod.NotAvailable -> {
|
||||
authControl = null
|
||||
error("No more auth method available")
|
||||
}
|
||||
components[AccountSecretsManager].saveSecrets(ssoContext.account, AccountSecretsImpl(client))
|
||||
registerClientOnline(handler)
|
||||
ssoContext.bot.logger.info { "Login successful." }
|
||||
|
||||
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))
|
||||
|
@ -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<Conn>(
|
||||
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`.
|
||||
|
@ -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
|
@ -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(
|
||||
|
@ -172,6 +172,7 @@ internal val NO_ENCRYPT: ByteArray = ByteArray(0)
|
||||
internal inline fun <R : Packet?> OutgoingPacketFactory<R>.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 <R : Packet?> OutgoingPacketFactory<R>.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
|
||||
|
@ -131,6 +131,7 @@ internal object KnownPacketFactories {
|
||||
object OutgoingFactories : List<OutgoingPacketFactory<*>> by mutableListOf(
|
||||
WtLogin.Login,
|
||||
WtLogin.ExchangeEmp,
|
||||
WtLogin.TransEmp,
|
||||
StatSvc.Register,
|
||||
StatSvc.GetOnlineStatus,
|
||||
StatSvc.SimpleGet,
|
||||
|
@ -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,
|
||||
|
@ -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<TransEmp.Response>("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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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, MiraiProtocolInternal>(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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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<MyLoginFailedException> { 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<IllegalStateException> { session.authByPassword(EMPTY_BYTE_ARRAY) }
|
||||
assertFailsWith<IllegalStateException> { session.authByPassword(EMPTY_BYTE_ARRAY) }
|
||||
assertFailsWith<IllegalStateException> { 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)
|
||||
|
||||
}
|
||||
}
|
@ -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<H : NetworkHandler> : 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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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<StackTraceElement>()
|
||||
}
|
||||
|
||||
override fun fillInStackTrace(): Throwable {
|
||||
stackTrace = EMPTY
|
||||
return this
|
||||
}
|
||||
|
||||
init {
|
||||
if (withStackTrace) super.fillInStackTrace()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
Loading…
Reference in New Issue
Block a user