diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index e141e1b4c..19a3d23c7 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -33,37 +33,53 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler launch(CoroutineName("Incoming Packet Receiver")) { processReceive() } bot.logger.info("Trying login") - when (val response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect()) { - is UnsafeLogin -> { - bot.logger.info("Login unsuccessful, device auth is needed") - bot.logger.info("登陆失败, 原因为非常用设备登陆") - bot.logger.info("Open the following URL in QQ browser and complete the verification") - bot.logger.info("将下面这个链接在QQ浏览器中打开并完成认证后尝试再次登陆") - bot.logger.info(response.url) - return - } + var response: LoginPacket.LoginPacketResponse = LoginPacket.SubCommand9(bot.client).sendAndExpect() + mainloop@ while (true) { + when (response) { + is UnsafeLogin -> { + bot.logger.info("Login unsuccessful, device auth is needed") + bot.logger.info("登陆失败, 原因为非常用设备登陆") + bot.logger.info("Open the following URL in QQ browser and complete the verification") + bot.logger.info("将下面这个链接在QQ浏览器中打开并完成认证后尝试再次登陆") + bot.logger.info(response.url) + return + } - is Captcha -> when (response) { - is Captcha.Picture -> { - bot.logger.info("需要图片验证码") - var result = bot.configuration.captchaSolver.invoke(bot, response.data) - if (result === null || result.length != 4) { - //refresh captcha - result = "ABCD" + is Captcha -> when (response) { + is Captcha.Picture -> { + bot.logger.info("需要图片验证码") + var result = bot.configuration.loginSolver.onSolvePicCaptcha(bot, response.data) + if (result === null || result.length != 4) { + //refresh captcha + result = "ABCD" + } + bot.logger.info("提交验证码") + response = LoginPacket.SubCommand2(bot.client, response.sign, result).sendAndExpect() + continue@mainloop + } + is Captcha.Slider -> { + bot.logger.info("需要滑动验证码") + TODO("滑动验证码") } - bot.logger.info("提交验证码") - val captchaResponse: LoginPacket.LoginPacketResponse = - LoginPacket.SubCommand2(bot.client, response.sign, result).sendAndExpect() } - is Captcha.Slider -> { - bot.logger.info("需要滑动验证码") + + is Error -> error(response.toString()) + + is SMSVerifyCodeNeeded -> { + val result = bot.configuration.loginSolver.onGetPhoneNumber() + response = LoginPacket.SubCommand7( + bot.client, + response.t174, + response.t402, + result + ).sendAndExpect() + continue@mainloop } - } - is Error -> error(response.toString()) - - is Success -> { - bot.logger.info("Login successful") + is Success -> { + bot.logger.info("Login successful") + break@mainloop + } } } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt index 008c8f70c..241d8e34d 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/Tlv.kt @@ -142,6 +142,7 @@ fun BytePacketBuilder.t116( } } + fun BytePacketBuilder.t100( appId: Long = 16, subAppId: Long = 537062845, @@ -193,6 +194,44 @@ fun BytePacketBuilder.t104( } } +fun BytePacketBuilder.t174( + t174Data: ByteArray +) { + writeShort(0x174) + writeShortLVPacket { + writeFully(t174Data) + } +} + +fun BytePacketBuilder.t19e( + value: Int = 0 +) { + writeShort(0x19e) + writeShortLVPacket { + writeShort(1) + writeByte(value.toByte()) + } +} + +fun BytePacketBuilder.t17c( + t17cData: ByteArray +) { + writeShort(0x17c) + writeShortLVPacket { + writeShort(t17cData.size.toShort()) + writeFully(t17cData) + } +} + +fun BytePacketBuilder.t401( + t401Data: ByteArray +) { + writeShort(0x401) + writeShortLVPacket { + writeFully(t401Data) + } +} + /** * @param apkId application.getPackageName().getBytes() */ diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt index 12a68b40f..953292005 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/LoginPacket.kt @@ -11,12 +11,9 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.* import net.mamoe.mirai.qqandroid.utils.GuidSource import net.mamoe.mirai.qqandroid.utils.MacOrAndroidIdChangeFlag import net.mamoe.mirai.qqandroid.utils.guidFlag -import net.mamoe.mirai.utils.MiraiDebugAPI -import net.mamoe.mirai.utils.MiraiInternalAPI +import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.cryptor.contentToString import net.mamoe.mirai.utils.cryptor.decryptBy -import net.mamoe.mirai.utils.currentTimeMillis -import net.mamoe.mirai.utils.currentTimeSeconds import net.mamoe.mirai.utils.io.* import net.mamoe.mirai.utils.io.discardExact @@ -49,6 +46,33 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt } } + object SubCommand7 { + private const val appId = 16L + private const val subAppId = 537062845L + + @UseExperimental(MiraiInternalAPI::class) + operator fun invoke( + client: QQAndroidClient, + t174: ByteArray, + t402: ByteArray, + phoneNumber: String + ): OutgoingPacket = buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId -> + writeSsoPacket(client, subAppId, commandName, sequenceId = sequenceId) { + writeOicqRequestPacket(client, EncryptMethodECDH7(client.ecdh), 0x0810) { + writeShort(7) // subCommand + writeShort(7) // count of TLVs, probably ignored by server?TODO + t8(2052) + t104(client.t104) + t116(150470524, 66560) + t174(t174) + t17c(phoneNumber.toByteArray()) + t401(md5(client.device.guid + "1234567890123456".toByteArray() + t402)) + t19e(0)//==tlv408 + } + } + } + } + object SubCommand9 { private const val appId = 16L private const val subAppId = 537062845L @@ -225,7 +249,7 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt class UnsafeLogin(val url: String) : LoginPacketResponse() - class DeviceLockLogin() : LoginPacketResponse() + class SMSVerifyCodeNeeded(val t174: ByteArray, val t402: ByteArray) : LoginPacketResponse() } @InternalAPI @@ -251,17 +275,18 @@ internal object LoginPacket : PacketFactory<LoginPacket.LoginPacketResponse>("wt 1, 15 -> onErrorMessage(tlvMap) 2 -> onSolveLoginCaptcha(tlvMap, bot) -96 -> onUnsafeDeviceLogin(tlvMap, bot) - -52 -> onDeviceLockLogin(tlvMap, bot) + -52 -> onSMSVerifyNeeded(tlvMap, bot) else -> error("unknown login result type: $type") } } - private fun onDeviceLockLogin(tlvMap: Map<Int, ByteArray>, bot: QQAndroidBot): LoginPacketResponse.DeviceLockLogin { - println(tlvMap[0x104]!!.toUHexString()) - println(tlvMap[0x402]!!.toUHexString()) - println(tlvMap[0x403]!!.toUHexString()) - return LoginPacketResponse.DeviceLockLogin(); + private fun onSMSVerifyNeeded( + tlvMap: Map<Int, ByteArray>, + bot: QQAndroidBot + ): LoginPacketResponse.SMSVerifyCodeNeeded { + bot.client.t104 = tlvMap[0x104]!! + return LoginPacketResponse.SMSVerifyCodeNeeded(tlvMap[0x174] ?: EMPTY_BYTE_ARRAY, tlvMap[0x402]!!) } private fun onUnsafeDeviceLogin(tlvMap: Map<Int, ByteArray>, bot: QQAndroidBot): LoginPacketResponse.UnsafeLogin { diff --git a/mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/TIMPCBotNetworkHandler.kt b/mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/TIMPCBotNetworkHandler.kt index 099a563e6..c6fea2d61 100644 --- a/mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/TIMPCBotNetworkHandler.kt +++ b/mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/TIMPCBotNetworkHandler.kt @@ -389,7 +389,7 @@ internal class TIMPCBotNetworkHandler internal constructor(coroutineContext: Cor close() return } - val code = configuration.captchaSolver(bot, captchaCache!!) + val code = configuration.loginSolver(bot, captchaCache!!) this.captchaCache = null if (code == null || code.length != 4) { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt index fc18b658b..8cbed1ba0 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt @@ -7,16 +7,21 @@ import kotlin.coroutines.EmptyCoroutineContext import kotlin.jvm.JvmStatic /** - * 验证码处理器. 需挂起(阻塞)直到处理完成验证码. - * - * 返回长度为 4 的验证码. 为空则刷新验证码 */ -typealias CaptchaSolver = suspend Bot.(IoBuffer) -> String? +abstract class LoginSolver { + abstract suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? + + abstract suspend fun onSolveSliderCaptcha(bot: Bot, data: IoBuffer): String? + + abstract suspend fun onGetPhoneNumber(): String + + abstract suspend fun onGetSMSVerifyCode(): String +} /** * 在各平台实现的默认的验证码处理器. */ -expect var DefaultCaptchaSolver: CaptchaSolver +expect var defaultLoginSolver: LoginSolver /** * 网络和连接配置 @@ -70,7 +75,7 @@ class BotConfiguration { /** * 验证码处理器 */ - var captchaSolver: CaptchaSolver = DefaultCaptchaSolver + var loginSolver: LoginSolver = defaultLoginSolver /** * 登录完成后几秒会收到好友消息的历史记录, * 这些历史记录不会触发事件. diff --git a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/DefaultCaptchaSolverJvm.kt b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/DefaultCaptchaSolverJvm.kt index b891eeb84..255ef5bc1 100644 --- a/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/DefaultCaptchaSolverJvm.kt +++ b/mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/utils/DefaultCaptchaSolverJvm.kt @@ -10,7 +10,9 @@ import kotlinx.coroutines.io.reader import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.io.core.IoBuffer import kotlinx.io.core.use +import net.mamoe.mirai.Bot import java.awt.Image import java.awt.image.BufferedImage import java.io.File @@ -23,31 +25,68 @@ import kotlin.coroutines.CoroutineContext * * 可被修改, 除覆盖配置外全局生效. */ -actual var DefaultCaptchaSolver: CaptchaSolver = { - captchaLock.withLock { - val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() } - withContext(Dispatchers.IO) { - tempFile.createNewFile() - MiraiLogger.info("需要验证码登录, 验证码为 4 字母") - try { - tempFile.writeChannel().use { writeFully(it) } - MiraiLogger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}") - } catch (e: Exception) { - MiraiLogger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片") - } +actual var defaultLoginSolver: LoginSolver = DefaultLoginSolver() - tempFile.inputStream().use { - val img = ImageIO.read(it) - if (img == null) { - MiraiLogger.info("无法创建字符图片. 请查看文件") - } else { - MiraiLogger.info(img.createCharImg()) + +class DefaultLoginSolver(): LoginSolver(){ + override suspend fun onSolvePicCaptcha(bot: Bot, data: IoBuffer): String? { + loginSolverLock.withLock { + val tempFile: File = createTempFile(suffix = ".png").apply { deleteOnExit() } + withContext(Dispatchers.IO) { + tempFile.createNewFile() + MiraiLogger.info("需要验证码登录, 验证码为 4 字母") + try { + tempFile.writeChannel().use { writeFully(data) } + MiraiLogger.info("将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}") + } catch (e: Exception) { + MiraiLogger.info("无法写出验证码文件(${e.message}), 请尝试查看以上字符图片") + } + + tempFile.inputStream().use { + val img = ImageIO.read(it) + if (img == null) { + MiraiLogger.info("无法创建字符图片. 请查看文件") + } else { + MiraiLogger.info(img.createCharImg()) + } + } + } + MiraiLogger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车") + return readLine()?.takeUnless { it.isEmpty() || it.length != 4 } + } + } + + override suspend fun onSolveSliderCaptcha(bot: Bot, data: IoBuffer): String? { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override suspend fun onGetPhoneNumber(): String { + loginSolverLock.withLock { + while (true){ + MiraiLogger.info("请输入你的手机号码") + val var0 = readLine() + if(var0!==null && var0.length > 10){ + return var0; } } } - MiraiLogger.info("请输入 4 位字母验证码. 若要更换验证码, 请直接回车") - readLine()?.takeUnless { it.isEmpty() || it.length != 4 } + return ""; } + + override suspend fun onGetSMSVerifyCode(): String { + loginSolverLock.withLock { + while (true){ + MiraiLogger.info("请输入你刚刚收到的手机验证码[6位数字]") + val var0 = readLine() + if(var0!==null && var0.length == 6){ + return var0; + } + } + } + return ""; + } + + } // Copied from Ktor CIO @@ -62,7 +101,7 @@ private fun File.writeChannel( }.channel -private val captchaLock = Mutex() +private val loginSolverLock = Mutex() /** * @author NaturalHG