1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-14 23:20:49 +08:00

Android SMS login - Incomplete

This commit is contained in:
jiahua.liu 2020-01-27 13:05:15 +08:00
parent 767b945a10
commit d8cd6625e5
6 changed files with 189 additions and 65 deletions
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network
mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network
mirai-core/src
commonMain/kotlin/net.mamoe.mirai/utils
jvmMain/kotlin/net/mamoe/mirai/utils

View File

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

View File

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

View File

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

View File

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

View File

@ -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
/**
* 登录完成后几秒会收到好友消息的历史记录,
* 这些历史记录不会触发事件.

View File

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