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:
StageGuard 2023-03-18 21:52:31 +08:00 committed by GitHub
parent e5ff458a5d
commit 78d0b4fd54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2817 additions and 482 deletions

View File

@ -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;

View File

@ -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")
}
}

View File

@ -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 ->

View File

@ -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()
}
}
}

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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;

View File

@ -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;

View File

@ -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)
}
}
}

View 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
}

View File

@ -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,
}
}

View File

@ -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 实现的异常

View File

@ -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`.")
}
/**
* 处理滑动验证码.
*

View File

@ -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()
}

View File

@ -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()
}
}

View 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>
}

View File

@ -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() }
)

View File

@ -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()
)

View 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() }
)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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),

View File

@ -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

View 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()
}
}

View 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 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)
}
}

View File

@ -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)
}
}

View 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 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()"
}
}
}

View 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)"
}

View File

@ -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()

View 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)"
}
}

View File

@ -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
)
)
}

View File

@ -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

View File

@ -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")
}
}

View File

@ -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.")
}
}
}

View File

@ -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))

View File

@ -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`.

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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")
}
}
}
}

View File

@ -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)

View File

@ -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)
}
}
}
}

View File

@ -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() }
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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)