mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-06 08:00:10 +08:00
New network: framework infrastructure
This commit is contained in:
parent
48564056df
commit
b844efb072
@ -44,6 +44,7 @@ object Versions {
|
||||
const val log4j = "2.13.3"
|
||||
const val asm = "9.1"
|
||||
const val difflib = "1.3.0"
|
||||
const val netty = "4.1.63.Final"
|
||||
|
||||
const val junit = "5.4.2"
|
||||
|
||||
@ -110,4 +111,5 @@ const val `jetbrains-annotations` = "org.jetbrains:annotations:19.0.0"
|
||||
|
||||
const val `caller-finder` = "io.github.karlatemp:caller:1.1.1"
|
||||
|
||||
const val `android-runtime` = "com.google.android:android:${Versions.android}"
|
||||
const val `android-runtime` = "com.google.android:android:${Versions.android}"
|
||||
const val `netty-all` = "io.netty:netty-all:${Versions.netty}"
|
@ -33,6 +33,10 @@ public suspend inline fun <R> runBIO(
|
||||
noinline block: () -> R
|
||||
): R = runInterruptible(context = Dispatchers.IO, block = block)
|
||||
|
||||
public suspend inline fun <T, R> T.runBIO(
|
||||
crossinline block: T.() -> R
|
||||
): R = runInterruptible(context = Dispatchers.IO, block = { block() })
|
||||
|
||||
public inline fun CoroutineScope.launchWithPermit(
|
||||
semaphore: Semaphore,
|
||||
coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||
|
54
mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt
Normal file
54
mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
public class ExceptionCollector {
|
||||
@Volatile
|
||||
private var last: Throwable? = null
|
||||
|
||||
@Synchronized
|
||||
public fun collect(e: Throwable?) {
|
||||
if (e == null) return
|
||||
val last = last
|
||||
if (last != null) {
|
||||
e.addSuppressed(last)
|
||||
}
|
||||
this.last = e
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to [collect] to be used inside [withExceptionCollector]
|
||||
*/
|
||||
public fun collectException(e: Throwable?): Unit = collect(e)
|
||||
|
||||
public fun getLast(): Throwable? = last
|
||||
|
||||
@TerminalOperation // to give it a color for a clearer control flow
|
||||
public fun collectThrow(exception: Throwable): Nothing {
|
||||
collect(exception)
|
||||
throw getLast()!!
|
||||
}
|
||||
|
||||
@DslMarker
|
||||
private annotation class TerminalOperation
|
||||
}
|
||||
|
||||
/**
|
||||
* Run with a coverage of `throw`. All thrown exceptions will be caught and rethrown with [ExceptionCollector.collectThrow]
|
||||
*/
|
||||
public inline fun <R> withExceptionCollector(action: ExceptionCollector.() -> R): R {
|
||||
ExceptionCollector().run {
|
||||
try {
|
||||
return action()
|
||||
} catch (e: Throwable) {
|
||||
collectThrow(e)
|
||||
}
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ kotlin {
|
||||
|
||||
api1(`kotlinx-io-jvm`)
|
||||
implementation1(`kotlinx-coroutines-io`)
|
||||
implementation(`netty-all`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import net.mamoe.mirai.internal.message.LongMessageInternal
|
||||
import net.mamoe.mirai.internal.network.*
|
||||
import net.mamoe.mirai.internal.network.handler.BdhSessionSyncer
|
||||
import net.mamoe.mirai.internal.network.handler.QQAndroidBotNetworkHandler
|
||||
import net.mamoe.mirai.internal.network.net.protocol.SsoContext
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
|
||||
@ -32,7 +33,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin
|
||||
import net.mamoe.mirai.internal.utils.ScheduledJob
|
||||
import net.mamoe.mirai.internal.utils.crypto.TEA
|
||||
import net.mamoe.mirai.internal.utils.friendCacheFile
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.message.data.ForwardMessage
|
||||
import net.mamoe.mirai.message.data.RichMessage
|
||||
@ -58,9 +58,9 @@ internal fun QQAndroidBot.createOtherClient(
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "BooleanLiteralArgument", "OverridingDeprecatedMember")
|
||||
internal class QQAndroidBot constructor(
|
||||
private val account: BotAccount,
|
||||
internal val account: BotAccount,
|
||||
configuration: BotConfiguration
|
||||
) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id) {
|
||||
) : AbstractBot<QQAndroidBotNetworkHandler>(configuration, account.id), SsoContext {
|
||||
val bdhSyncer: BdhSessionSyncer = BdhSessionSyncer(this)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@ -101,38 +101,9 @@ internal class QQAndroidBot constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSecretsFromCacheOrCreate(deviceInfo: DeviceInfo): AccountSecrets {
|
||||
val loaded = if (configuration.loginCacheEnabled && accountSecretsFile.exists()) {
|
||||
kotlin.runCatching {
|
||||
TEA.decrypt(accountSecretsFile.readBytes(), account.passwordMd5).loadAs(AccountSecretsImpl.serializer())
|
||||
}.getOrElse { e ->
|
||||
logger.error("Failed to load account secrets from local cache. Invalidating cache...", e)
|
||||
accountSecretsFile.delete()
|
||||
null
|
||||
}
|
||||
} else null
|
||||
if (loaded != null) {
|
||||
logger.info { "Loaded account secrets from local cache." }
|
||||
return loaded
|
||||
}
|
||||
|
||||
return AccountSecretsImpl(deviceInfo, account) // wLoginSigInfoField is null, no need to save.
|
||||
}
|
||||
|
||||
/////////////////////////// accounts secrets end
|
||||
|
||||
var client: QQAndroidClient = initClient()
|
||||
|
||||
fun initClient(): QQAndroidClient {
|
||||
val device = configuration.deviceInfo?.invoke(this) ?: DeviceInfo.random()
|
||||
client = QQAndroidClient(
|
||||
account,
|
||||
device = device,
|
||||
accountSecrets = loadSecretsFromCacheOrCreate(device)
|
||||
)
|
||||
client._bot = this
|
||||
return client
|
||||
}
|
||||
override lateinit var client: QQAndroidClient
|
||||
|
||||
|
||||
override val bot: QQAndroidBot get() = this
|
||||
|
@ -25,7 +25,7 @@ internal interface AccountSecrets {
|
||||
var wLoginSigInfoField: WLoginSigInfo?
|
||||
|
||||
val wLoginSigInfoInitialized get() = wLoginSigInfoField != null
|
||||
var wLoginSigInfo
|
||||
var wLoginSigInfo: WLoginSigInfo
|
||||
get() = wLoginSigInfoField ?: error("wLoginSigInfoField is not yet initialized")
|
||||
set(value) {
|
||||
wLoginSigInfoField = value
|
||||
@ -49,12 +49,13 @@ internal interface AccountSecrets {
|
||||
val randomKey: ByteArray
|
||||
}
|
||||
|
||||
@Suppress("ArrayInDataClass") // for `copy`
|
||||
@Serializable
|
||||
internal class AccountSecretsImpl(
|
||||
internal data class AccountSecretsImpl(
|
||||
override var loginExtraData: MutableSet<LoginExtraData>,
|
||||
override var wLoginSigInfoField: WLoginSigInfo?,
|
||||
override var G: ByteArray,
|
||||
override var dpwd: ByteArray,
|
||||
override var dpwd: ByteArray = get_mpasswd().toByteArray(),
|
||||
override var randSeed: ByteArray,
|
||||
override var ksid: ByteArray,
|
||||
override var tgtgtKey: ByteArray,
|
||||
|
@ -22,6 +22,7 @@ import kotlinx.serialization.Serializable
|
||||
import net.mamoe.mirai.data.OnlineStatus
|
||||
import net.mamoe.mirai.internal.BotAccount
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.network.net.protocol.LoginSessionAware
|
||||
import net.mamoe.mirai.internal.network.protocol.SyncingCacheList
|
||||
import net.mamoe.mirai.internal.network.protocol.data.jce.FileStoragePushFSSvcList
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||
@ -65,13 +66,15 @@ internal val DefaultServerList: MutableSet<Pair<String, Int>> =
|
||||
DOMAINS
|
||||
Pskey: "openmobile.qq.com"
|
||||
*/
|
||||
@PublishedApi
|
||||
/**
|
||||
* holds all the states related to network.
|
||||
*/
|
||||
internal open class QQAndroidClient(
|
||||
val account: BotAccount,
|
||||
val ecdh: ECDH = ECDH(),
|
||||
override val ecdh: ECDH = ECDH(),
|
||||
val device: DeviceInfo,
|
||||
accountSecrets: AccountSecrets
|
||||
) : AccountSecrets by accountSecrets {
|
||||
) : AccountSecrets by accountSecrets, LoginSessionAware {
|
||||
lateinit var _bot: QQAndroidBot
|
||||
val bot: QQAndroidBot get() = _bot
|
||||
|
||||
@ -124,6 +127,7 @@ internal open class QQAndroidClient(
|
||||
return new
|
||||
}
|
||||
|
||||
// TODO: 2021/4/14 investigate whether they can be minimized
|
||||
private val friendSeq: AtomicInt = atomic(getRandomUnsignedInt())
|
||||
internal fun getFriendSeq(): Int = friendSeq.value
|
||||
|
||||
@ -213,8 +217,8 @@ internal open class QQAndroidClient(
|
||||
@PublishedApi
|
||||
internal val apkId: ByteArray = "com.tencent.mobileqq".toByteArray()
|
||||
|
||||
var outgoingPacketSessionId: ByteArray = 0x02B05B8B.toByteArray()
|
||||
var loginState = 0
|
||||
override var outgoingPacketSessionId: ByteArray = 0x02B05B8B.toByteArray()
|
||||
override var loginState = 0
|
||||
|
||||
var t150: Tlv? = null
|
||||
var rollbackSig: ByteArray? = null
|
||||
|
@ -67,8 +67,9 @@ internal class LoginExtraData(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ArrayInDataClass") // for `copy`
|
||||
@Serializable
|
||||
internal class WLoginSigInfo(
|
||||
internal data class WLoginSigInfo(
|
||||
val uin: Long,
|
||||
var encryptA1: ByteArray?, // sigInfo[0]
|
||||
/**
|
||||
|
213
mirai-core/src/commonMain/kotlin/network/net/NetworkHandler.kt
Normal file
213
mirai-core/src/commonMain/kotlin/network/net/NetworkHandler.kt
Normal file
@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network.net
|
||||
|
||||
import kotlinx.atomicfu.atomic
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandler.State
|
||||
import net.mamoe.mirai.internal.network.net.protocol.SsoController
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.utils.BotConfiguration
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.SocketAddress
|
||||
import java.util.concurrent.CancellationException
|
||||
|
||||
/**
|
||||
* Immutable context for [NetworkHandler]
|
||||
*/
|
||||
internal interface NetworkHandlerContext {
|
||||
val bot: QQAndroidBot
|
||||
|
||||
val logger: MiraiLogger
|
||||
val ssoController: SsoController
|
||||
val configuration: BotConfiguration
|
||||
|
||||
fun getNextAddress(): SocketAddress // FIXME: 2021/4/14
|
||||
}
|
||||
|
||||
internal class NetworkHandlerContextImpl(
|
||||
override val bot: QQAndroidBot,
|
||||
override val ssoController: SsoController,
|
||||
) : NetworkHandlerContext {
|
||||
override val configuration: BotConfiguration
|
||||
get() = bot.configuration
|
||||
|
||||
override fun getNextAddress(): SocketAddress {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override val logger: MiraiLogger by lazy { configuration.networkLoggerSupplier(bot) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic interface available to application. Usually wrapped with [SelectorNetworkHandler].
|
||||
*
|
||||
* A [NetworkHandler] holds no reference to [Bot]s.
|
||||
*/
|
||||
internal interface NetworkHandler {
|
||||
val context: NetworkHandlerContext
|
||||
|
||||
/**
|
||||
* State of this handler.
|
||||
*/
|
||||
val state: State
|
||||
|
||||
enum class State {
|
||||
/**
|
||||
* Just created and no connection has been made.
|
||||
*
|
||||
* At this state [resumeConnection] turns state into [CONNECTING] and
|
||||
* establishes a connection to the server and do authentication, for which [sendAndExpect] suspends.
|
||||
*/
|
||||
INITIALIZED,
|
||||
|
||||
/**
|
||||
* Connection to server, including the process of authentication.
|
||||
*
|
||||
* At this state [resumeConnection] does nothing. [sendAndExpect] suspends for the result of connection started in [INITIALIZED].
|
||||
*/
|
||||
CONNECTING,
|
||||
|
||||
/**
|
||||
* Everything is working. [resumeConnection] does nothing. [sendAndExpect] does not suspend for connection reasons.
|
||||
*/
|
||||
OK,
|
||||
|
||||
/**
|
||||
* No Internet Connection available or for any other reasons but it is possible to establish a connection again(switching state to [CONNECTING]).
|
||||
*/
|
||||
CONNECTION_LOST,
|
||||
|
||||
/**
|
||||
* Cannot resume anymore. Both [resumeConnection] and [sendAndExpect] throw a [CancellationException].
|
||||
*
|
||||
* When a handler reached [CLOSED] state, it is finalized and cannot be restored to any other states.
|
||||
*/
|
||||
CLOSED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to resume the connection. Throws no exception but changes [state]
|
||||
* @see State
|
||||
*/
|
||||
suspend fun resumeConnection()
|
||||
|
||||
|
||||
/**
|
||||
* Sends [packet] and expects to receive a response from the server.
|
||||
* @param attempts ranges `1..INFINITY`
|
||||
*/
|
||||
suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long = 5000, attempts: Int = 2): Packet?
|
||||
|
||||
/**
|
||||
* Sends [packet] and does not expect any response. (Response is still processed but not passed as a return value of this function.)
|
||||
*/
|
||||
suspend fun sendWithoutExpect(packet: OutgoingPacket)
|
||||
|
||||
|
||||
/**
|
||||
* Closes this handler gracefully and suspends the coroutine for its completion.
|
||||
*/
|
||||
suspend fun close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for a specific [NetworkHandler] implementation.
|
||||
*/
|
||||
internal interface NetworkHandlerFactory<H : NetworkHandler> {
|
||||
fun create(context: NetworkHandlerContext, host: String, port: Int): H =
|
||||
create(context, InetSocketAddress.createUnresolved(host, port))
|
||||
|
||||
fun create(context: NetworkHandlerContext, host: InetAddress, port: Int): H =
|
||||
create(context, InetSocketAddress(host, port))
|
||||
|
||||
/**
|
||||
* Create an instance of [H]. The returning [H] has [NetworkHandler.state] of [State.INITIALIZED]
|
||||
*/
|
||||
fun create(context: NetworkHandlerContext, address: SocketAddress): H
|
||||
}
|
||||
|
||||
/**
|
||||
* A lazy stateful selector of [NetworkHandler].
|
||||
*
|
||||
* - Calls [factory.create][NetworkHandlerFactory.create] to create [NetworkHandler]s.
|
||||
* - Re-initialize [NetworkHandler] instances if the old one is dead.
|
||||
* - Suspends requests when connection is not available.
|
||||
*
|
||||
* No connection is created until first invocation of [getResumedInstance],
|
||||
* and new connections are created only when calling [getResumedInstance] if the old connection was dead.
|
||||
*/
|
||||
internal abstract class NetworkHandlerSelector<H : NetworkHandler> {
|
||||
/**
|
||||
* Returns an instance immediately without suspension, or `null` if instance not ready.
|
||||
* @see awaitResumeInstance
|
||||
*/
|
||||
abstract fun getResumedInstance(): H?
|
||||
|
||||
/**
|
||||
* Returns an alive [NetworkHandler], or suspends the coroutine until the connection has been made again.
|
||||
*/
|
||||
abstract suspend fun awaitResumeInstance(): H
|
||||
}
|
||||
|
||||
// TODO: 2021/4/14 better naming
|
||||
internal abstract class AutoReconnectNetworkHandlerSelector<H : NetworkHandler> : NetworkHandlerSelector<H>() {
|
||||
private val current = atomic<H?>(null)
|
||||
|
||||
protected abstract fun createInstance(): H
|
||||
|
||||
final override fun getResumedInstance(): H? = current.value
|
||||
|
||||
final override tailrec suspend fun awaitResumeInstance(): H {
|
||||
val current = getResumedInstance()
|
||||
return if (current != null) {
|
||||
when (current.state) {
|
||||
State.OK -> current
|
||||
State.CLOSED -> {
|
||||
this.current.compareAndSet(current, null) // invalidate the instance and try again.
|
||||
awaitResumeInstance()
|
||||
}
|
||||
else -> {
|
||||
current.resumeConnection() // try to advance state.
|
||||
awaitResumeInstance()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.current.compareAndSet(current, createInstance())
|
||||
awaitResumeInstance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates [NetworkHandler] calls to instance returned by [NetworkHandlerSelector.awaitResumeInstance].
|
||||
*/
|
||||
internal class SelectorNetworkHandler(
|
||||
override val context: NetworkHandlerContext,
|
||||
private val selector: NetworkHandlerSelector<*>
|
||||
) : NetworkHandler {
|
||||
private suspend inline fun instance(): NetworkHandler = selector.awaitResumeInstance()
|
||||
|
||||
override val state: State get() = selector.getResumedInstance()?.state ?: State.INITIALIZED
|
||||
|
||||
override suspend fun resumeConnection() {
|
||||
instance() // the selector will resume connection for us.
|
||||
}
|
||||
|
||||
override suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long, attempts: Int) =
|
||||
instance().sendAndExpect(packet, timeout, attempts)
|
||||
|
||||
override suspend fun sendWithoutExpect(packet: OutgoingPacket) = instance().sendWithoutExpect(packet)
|
||||
override suspend fun close() = instance().close()
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network.net.impl.netty
|
||||
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufInputStream
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter
|
||||
import io.netty.channel.ChannelInitializer
|
||||
import io.netty.channel.SimpleChannelInboundHandler
|
||||
import io.netty.channel.nio.NioEventLoopGroup
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import io.netty.handler.codec.LengthFieldBasedFrameDecoder
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.sendBlocking
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandler
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandlerContext
|
||||
import net.mamoe.mirai.internal.network.net.protocol.PacketCodec
|
||||
import net.mamoe.mirai.internal.network.net.protocol.RawIncomingPacket
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.utils.childScope
|
||||
import java.net.SocketAddress
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
internal class NettyNetworkHandler(
|
||||
context: NetworkHandlerContext,
|
||||
private val address: SocketAddress,
|
||||
) : NetworkHandlerSupport(context) {
|
||||
override suspend fun close() {
|
||||
super.close()
|
||||
setState(StateClosed())
|
||||
}
|
||||
|
||||
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||
val state = _state as NettyState
|
||||
state.sendPacketImpl(packet)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// netty conn.
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private inner class ByteBufToIncomingPacketDecoder : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
|
||||
ctx.fireChannelRead(msg.toReadPacket().use { packet ->
|
||||
PacketCodec.decodeRaw(context.bot.client, packet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RawIncomingPacketCollector(
|
||||
private val decodePipeline: PacketDecodePipeline
|
||||
) : SimpleChannelInboundHandler<RawIncomingPacket>(RawIncomingPacket::class.java) {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: RawIncomingPacket) {
|
||||
decodePipeline.send(msg)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createConnection(decodePipeline: PacketDecodePipeline): ChannelHandlerContext {
|
||||
val contextResult = CompletableDeferred<ChannelHandlerContext>()
|
||||
|
||||
val eventLoopGroup = NioEventLoopGroup()
|
||||
Bootstrap().group(eventLoopGroup)
|
||||
.channel(NioSocketChannel::class.java)
|
||||
.handler(object : ChannelInitializer<SocketChannel>() {
|
||||
override fun initChannel(ch: SocketChannel) {
|
||||
ch.pipeline().addLast(object : ChannelInboundHandlerAdapter() {
|
||||
override fun channelActive(ctx: ChannelHandlerContext) {
|
||||
contextResult.complete(ctx)
|
||||
}
|
||||
})
|
||||
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 0))
|
||||
.addLast(ByteBufToIncomingPacketDecoder())
|
||||
.addLast(RawIncomingPacketCollector(decodePipeline))
|
||||
}
|
||||
})
|
||||
.connect(address).runBIO { await() }
|
||||
// TODO: 2021/4/14 eventLoopGroup 移动到 bot, 并在 bot.close() 时关闭
|
||||
|
||||
return contextResult.await()
|
||||
}
|
||||
|
||||
private inner class PacketDecodePipeline(parentContext: CoroutineContext) :
|
||||
CoroutineScope by parentContext.childScope() {
|
||||
private val channel: Channel<RawIncomingPacket> = Channel(Channel.BUFFERED)
|
||||
|
||||
init {
|
||||
launch(CoroutineName("PacketDecodePipeline processor")) {
|
||||
// 'single thread' processor
|
||||
channel.consumeAsFlow().collect { raw ->
|
||||
val result = PacketCodec.processBody(context.bot, raw)
|
||||
if (result == null) {
|
||||
collectUnknownPacket(raw)
|
||||
} else collectReceived(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun send(raw: RawIncomingPacket) = channel.sendBlocking(raw)
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// states
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private abstract inner class NettyState(
|
||||
correspondingState: NetworkHandler.State
|
||||
) : BaseStateImpl(correspondingState) {
|
||||
abstract suspend fun sendPacketImpl(packet: OutgoingPacket)
|
||||
}
|
||||
|
||||
private inner class StateInitialized : NettyState(NetworkHandler.State.INITIALIZED) {
|
||||
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||
error("Cannot send packet when connection is not set. (resumeConnection not called.)")
|
||||
}
|
||||
|
||||
override suspend fun resumeConnection() {
|
||||
setState(StateConnecting(PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext)))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class StateConnecting(
|
||||
val decodePipeline: PacketDecodePipeline,
|
||||
) : NettyState(NetworkHandler.State.CONNECTING) {
|
||||
private val connection = async {
|
||||
createConnection(decodePipeline)
|
||||
}
|
||||
|
||||
private val connectResult = async {
|
||||
val connection = connection.await()
|
||||
context.ssoController.login()
|
||||
setState(StateOK(connection))
|
||||
}.apply {
|
||||
invokeOnCompletion { error ->
|
||||
if (error != null) setState(StateClosed()) // logon failure closes the network handler.
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||
connection.await().writeAndFlush(packet)
|
||||
}
|
||||
|
||||
override suspend fun resumeConnection() {
|
||||
connectResult.await() // propagates exceptions
|
||||
}
|
||||
}
|
||||
|
||||
private inner class StateOK(
|
||||
private val connection: ChannelHandlerContext
|
||||
) : NettyState(NetworkHandler.State.OK) {
|
||||
override suspend fun sendPacketImpl(packet: OutgoingPacket) {
|
||||
connection.writeAndFlush(packet)
|
||||
}
|
||||
|
||||
override suspend fun resumeConnection() {} // noop
|
||||
}
|
||||
|
||||
private inner class StateClosed : NettyState(NetworkHandler.State.OK) {
|
||||
override suspend fun sendPacketImpl(packet: OutgoingPacket) = error("NetworkHandler is already closed.")
|
||||
override suspend fun resumeConnection() {} // noop
|
||||
}
|
||||
|
||||
override fun initialState(): BaseStateImpl = StateInitialized()
|
||||
}
|
||||
|
||||
private fun ByteBuf.toReadPacket(): ByteReadPacket {
|
||||
val buf = this
|
||||
return buildPacket {
|
||||
ByteBufInputStream(buf).withUse { copyTo(outputStream()) }
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network.net.impl.netty
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandler
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandlerContext
|
||||
import net.mamoe.mirai.internal.network.net.protocol.RawIncomingPacket
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacket
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
private val PACKET_DEBUG = systemProp("mirai.debug.packet.logger", true)
|
||||
|
||||
internal abstract class NetworkHandlerSupport(
|
||||
override val context: NetworkHandlerContext,
|
||||
final override val coroutineContext: CoroutineContext = SupervisorJob(),
|
||||
) : NetworkHandler, CoroutineScope by coroutineContext.childScope(SupervisorJob()) {
|
||||
|
||||
protected abstract fun initialState(): BaseStateImpl
|
||||
protected abstract suspend fun sendPacketImpl(packet: OutgoingPacket)
|
||||
|
||||
/**
|
||||
* Called when a packet is received.
|
||||
*/
|
||||
protected fun collectReceived(packet: IncomingPacket) {
|
||||
for (listener in packetListeners) {
|
||||
if (!listener.isExpected(packet)) continue
|
||||
if (packetListeners.remove(listener)) {
|
||||
val e = packet.exception
|
||||
if (e != null) {
|
||||
listener.result.completeExceptionally(e)
|
||||
} else {
|
||||
listener.result.complete(packet.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun collectUnknownPacket(raw: RawIncomingPacket) {
|
||||
packetLogger.debug { "Unknown packet: commandName=${raw.commandName}, body=${raw.body.toUHexString()}" }
|
||||
// may add hooks here (to context)
|
||||
}
|
||||
|
||||
final override suspend fun sendAndExpect(packet: OutgoingPacket, timeout: Long, attempts: Int): Packet? {
|
||||
val listener = PacketListener(packet.commandName, packet.sequenceId)
|
||||
packetListeners.add(listener)
|
||||
var exception: Throwable? = null
|
||||
repeat(attempts.coerceAtLeast(1)) {
|
||||
try {
|
||||
sendPacketImpl(packet)
|
||||
try {
|
||||
return withTimeout(timeout) {
|
||||
listener.result.await()
|
||||
}
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
if (exception != null) {
|
||||
e.addSuppressed(exception!!)
|
||||
}
|
||||
exception = e // show last exception
|
||||
}
|
||||
} finally {
|
||||
packetListeners.remove()
|
||||
}
|
||||
}
|
||||
throw exception!!
|
||||
}
|
||||
|
||||
final override suspend fun sendWithoutExpect(packet: OutgoingPacket) {
|
||||
sendPacketImpl(packet)
|
||||
}
|
||||
|
||||
override suspend fun close() {
|
||||
coroutineContext.job.cancel("NetworkHandler closed.")
|
||||
}
|
||||
|
||||
protected val packetLogger: MiraiLogger by lazy {
|
||||
MiraiLogger.create(context.logger.identity + ".debug").withSwitch(PACKET_DEBUG)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// await impl
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected class PacketListener(
|
||||
val commandName: String,
|
||||
val sequenceId: Int,
|
||||
) {
|
||||
/**
|
||||
* Response from server. May complete with [CompletableDeferred.completeExceptionally] for a meaningful stacktrace.
|
||||
*/
|
||||
val result = CompletableDeferred<Packet?>()
|
||||
|
||||
fun isExpected(packet: IncomingPacket): Boolean =
|
||||
this.commandName == packet.commandName && this.sequenceId == packet.sequenceId
|
||||
}
|
||||
|
||||
private val packetListeners = ConcurrentLinkedQueue<PacketListener>()
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// state impl
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* A **scoped** state corresponding to [NetworkHandler.State].
|
||||
*
|
||||
* CoroutineScope is cancelled when switched to another state.
|
||||
*/
|
||||
protected abstract inner class BaseStateImpl(
|
||||
val correspondingState: NetworkHandler.State,
|
||||
) : CoroutineScope by CoroutineScope(coroutineContext + SupervisorJob(coroutineContext.job)) {
|
||||
@Throws(Exception::class)
|
||||
abstract suspend fun resumeConnection()
|
||||
}
|
||||
|
||||
/**
|
||||
* State is *lazy*, initialized only if requested.
|
||||
*/
|
||||
@Suppress("PropertyName")
|
||||
protected var _state: BaseStateImpl by lateinitMutableProperty { initialState() }
|
||||
private set
|
||||
|
||||
final override val state: NetworkHandler.State get() = _state.correspondingState
|
||||
protected fun setState(impl: BaseStateImpl) { // we can add monitor here for debug.
|
||||
val old = _state
|
||||
check(old !== impl) { "Old and new states cannot be the same." }
|
||||
old.cancel()
|
||||
_state = impl
|
||||
}
|
||||
|
||||
final override suspend fun resumeConnection() {
|
||||
_state.resumeConnection()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network.net.impl.netty
|
||||
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandlerContext
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandlerFactory
|
||||
import java.net.SocketAddress
|
||||
|
||||
internal object NettyNetworkHandlerFactory : NetworkHandlerFactory<NettyNetworkHandler> {
|
||||
override fun create(context: NetworkHandlerContext, address: SocketAddress): NettyNetworkHandler {
|
||||
return NettyNetworkHandler(context, address)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network.net.protocol
|
||||
|
||||
import net.mamoe.mirai.internal.network.AccountSecrets
|
||||
import net.mamoe.mirai.internal.network.WLoginSigInfo
|
||||
import net.mamoe.mirai.internal.utils.crypto.ECDH
|
||||
|
||||
/**
|
||||
* @see AccountSecrets
|
||||
*/
|
||||
internal interface LoginSessionAware {
|
||||
var outgoingPacketSessionId: ByteArray
|
||||
|
||||
/**
|
||||
* always 0 for now.
|
||||
*/
|
||||
var loginState: Int
|
||||
val ecdh: ECDH
|
||||
|
||||
// also present in AccountSecrets
|
||||
var wLoginSigInfo: WLoginSigInfo
|
||||
val randomKey: ByteArray
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network.net.protocol
|
||||
|
||||
import kotlinx.io.core.*
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.*
|
||||
import net.mamoe.mirai.internal.utils.crypto.TEA
|
||||
import net.mamoe.mirai.internal.utils.crypto.adjustToPublicKey
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.io.use
|
||||
|
||||
internal object PacketCodec {
|
||||
/**
|
||||
* It's caller's responsibility to close [input]
|
||||
* @param input received from sockets.
|
||||
* @return decoded
|
||||
*/
|
||||
fun decodeRaw(client: LoginSessionAware, input: ByteReadPacket): RawIncomingPacket = input.run {
|
||||
// login
|
||||
val flag1 = readInt()
|
||||
|
||||
PacketLogger.verbose { "开始处理一个包" }
|
||||
|
||||
val flag2 = readByte().toInt()
|
||||
val flag3 = readByte().toInt()
|
||||
check(flag3 == 0) {
|
||||
"Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " +
|
||||
"Remaining=${this.readBytes().toUHexString()}"
|
||||
}
|
||||
|
||||
readString(readInt() - 4)// uinAccount
|
||||
|
||||
ByteArrayPool.useInstance(this.remaining.toInt()) { buffer ->
|
||||
val size = this.readAvailable(buffer)
|
||||
|
||||
when (flag2) {
|
||||
2 -> TEA.decrypt(buffer, DECRYPTER_16_ZERO, size)
|
||||
1 -> TEA.decrypt(buffer, client.wLoginSigInfo.d2Key, size)
|
||||
0 -> buffer
|
||||
else -> error("Unknown flag2=$flag2")
|
||||
}.let { decryptedData ->
|
||||
when (flag1) {
|
||||
0x0A -> parseSsoFrame(client, decryptedData)
|
||||
0x0B -> parseSsoFrame(client, decryptedData) // 这里可能是 uni?? 但测试时候发现结构跟 sso 一样.
|
||||
else -> error("unknown flag1: ${flag1.toByte().toUHexString()}")
|
||||
}
|
||||
}.let { raw ->
|
||||
when (flag2) {
|
||||
0, 1 -> RawIncomingPacket(raw.commandName, raw.sequenceId, raw.body.readBytes())
|
||||
2 -> RawIncomingPacket(
|
||||
raw.commandName,
|
||||
raw.sequenceId,
|
||||
raw.body.withUse { parseOicqResponse(client) })
|
||||
else -> error("Unknown flag2=$flag2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DecodeResult constructor(
|
||||
val commandName: String,
|
||||
val sequenceId: Int,
|
||||
/**
|
||||
* Can be passed to [PacketFactory]
|
||||
*/
|
||||
val body: ByteReadPacket,
|
||||
)
|
||||
|
||||
private fun parseSsoFrame(client: LoginSessionAware, bytes: ByteArray): DecodeResult =
|
||||
bytes.toReadPacket().use { input ->
|
||||
val commandName: String
|
||||
val ssoSequenceId: Int
|
||||
val dataCompressed: Int
|
||||
input.readPacketExact(input.readInt() - 4).withUse {
|
||||
ssoSequenceId = readInt()
|
||||
PacketLogger.verbose { "sequenceId = $ssoSequenceId" }
|
||||
|
||||
val returnCode = readInt()
|
||||
check(returnCode == 0) {
|
||||
if (returnCode <= -10000) {
|
||||
// https://github.com/mamoe/mirai/issues/470
|
||||
throw KnownPacketFactories.PacketFactoryIllegalStateException(
|
||||
returnCode,
|
||||
"returnCode = $returnCode"
|
||||
)
|
||||
} else "returnCode = $returnCode"
|
||||
}
|
||||
|
||||
if (PacketLogger.isEnabled) {
|
||||
val extraData = readBytes(readInt() - 4)
|
||||
PacketLogger.verbose { "(sso/inner)extraData = ${extraData.toUHexString()}" }
|
||||
} else {
|
||||
discardExact(readInt() - 4)
|
||||
}
|
||||
|
||||
commandName = readString(readInt() - 4)
|
||||
client.outgoingPacketSessionId = readBytes(readInt() - 4)
|
||||
|
||||
dataCompressed = readInt()
|
||||
}
|
||||
|
||||
val packet = when (dataCompressed) {
|
||||
0 -> {
|
||||
val size = input.readInt().toLong() and 0xffffffff
|
||||
if (size == input.remaining || size == input.remaining + 4) {
|
||||
input
|
||||
} else {
|
||||
buildPacket {
|
||||
writeInt(size.toInt())
|
||||
writePacket(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
input.discardExact(4)
|
||||
input.useBytes { data, length ->
|
||||
data.unzip(0, length).let {
|
||||
val size = it.toInt()
|
||||
if (size == it.size || size == it.size + 4) {
|
||||
it.toReadPacket(offset = 4)
|
||||
} else {
|
||||
it.toReadPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8 -> input
|
||||
else -> error("unknown dataCompressed flag: $dataCompressed")
|
||||
}
|
||||
|
||||
// body
|
||||
|
||||
return DecodeResult(commandName, ssoSequenceId, packet)
|
||||
}
|
||||
|
||||
private fun ByteReadPacket.parseOicqResponse(
|
||||
client: LoginSessionAware,
|
||||
): ByteArray {
|
||||
check(readByte().toInt() == 2)
|
||||
this.discardExact(2)
|
||||
this.discardExact(2)
|
||||
this.readUShort()
|
||||
this.readShort()
|
||||
this.readUInt().toLong()
|
||||
val encryptionMethod = this.readUShort().toInt()
|
||||
|
||||
this.discardExact(1)
|
||||
return when (encryptionMethod) {
|
||||
4 -> {
|
||||
val data =
|
||||
TEA.decrypt(
|
||||
this.readBytes(),
|
||||
client.ecdh.keyPair.initialShareKey,
|
||||
length = (this.remaining - 1).toInt()
|
||||
)
|
||||
|
||||
val peerShareKey =
|
||||
client.ecdh.calculateShareKeyByPeerPublicKey(readUShortLVByteArray().adjustToPublicKey())
|
||||
TEA.decrypt(data, peerShareKey)
|
||||
}
|
||||
3 -> {
|
||||
// session
|
||||
TEA.decrypt(
|
||||
this.readBytes(),
|
||||
client.wLoginSigInfo.wtSessionTicketKey,
|
||||
length = (this.remaining - 1).toInt()
|
||||
)
|
||||
}
|
||||
0 -> {
|
||||
if (client.loginState == 0) {
|
||||
val size = (this.remaining - 1).toInt()
|
||||
val byteArrayBuffer = this.readBytes(size)
|
||||
|
||||
runCatching {
|
||||
TEA.decrypt(byteArrayBuffer, client.ecdh.keyPair.initialShareKey, length = size)
|
||||
}.getOrElse {
|
||||
TEA.decrypt(byteArrayBuffer, client.randomKey, length = size)
|
||||
}
|
||||
} else {
|
||||
TEA.decrypt(this.readBytes(), client.randomKey, length = (this.remaining - 1).toInt())
|
||||
}
|
||||
}
|
||||
else -> error("Illegal encryption method. expected 0 or 4, got $encryptionMethod")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process [RawIncomingPacket] using [IncomingPacketFactory.decode].
|
||||
*
|
||||
* This function wraps exceptions into [IncomingPacket]
|
||||
*/
|
||||
suspend fun processBody(bot: QQAndroidBot, input: RawIncomingPacket): IncomingPacket? {
|
||||
val factory = KnownPacketFactories.findPacketFactory(input.commandName) ?: return null
|
||||
|
||||
return kotlin.runCatching {
|
||||
input.body.toReadPacket().use { body ->
|
||||
when (factory) {
|
||||
is OutgoingPacketFactory -> factory.decode(bot, body)
|
||||
is IncomingPacketFactory -> factory.decode(bot, body, input.sequenceId)
|
||||
}
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = { packet ->
|
||||
IncomingPacket(input.commandName, input.sequenceId, packet, null)
|
||||
},
|
||||
onFailure = { exception: Throwable ->
|
||||
IncomingPacket(input.commandName, input.sequenceId, null, exception)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal open class RawIncomingPacket constructor(
|
||||
val commandName: String,
|
||||
val sequenceId: Int,
|
||||
/**
|
||||
* Can be passed to [PacketFactory]
|
||||
*/
|
||||
val body: ByteArray,
|
||||
)
|
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network.net.protocol
|
||||
|
||||
import net.mamoe.mirai.internal.network.AccountSecrets
|
||||
import net.mamoe.mirai.internal.network.AccountSecretsImpl
|
||||
import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.internal.network.QQAndroidClient
|
||||
import net.mamoe.mirai.internal.network.net.NetworkHandler
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketWithRespType
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin10
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin2
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin20
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.WtLogin9
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
|
||||
import net.mamoe.mirai.internal.utils.crypto.TEA
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.network.*
|
||||
import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol
|
||||
import net.mamoe.mirai.utils.DeviceInfo
|
||||
import net.mamoe.mirai.utils.LoginSolver
|
||||
import net.mamoe.mirai.utils.info
|
||||
import net.mamoe.mirai.utils.withExceptionCollector
|
||||
import java.io.File
|
||||
|
||||
internal interface SsoContext {
|
||||
var client: QQAndroidClient
|
||||
}
|
||||
|
||||
internal class SsoController(
|
||||
private val ssoContext: SsoContext,
|
||||
private val handler: NetworkHandler,
|
||||
) {
|
||||
@Throws(LoginFailedException::class)
|
||||
suspend fun login() = withExceptionCollector {
|
||||
if (bot.client.wLoginSigInfoInitialized) {
|
||||
kotlin.runCatching {
|
||||
fastLogin()
|
||||
}.onFailure { e ->
|
||||
collectException(e)
|
||||
slowLogin()
|
||||
}
|
||||
} else {
|
||||
slowLogin()
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// impl
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private val configuration get() = handler.context.configuration
|
||||
private val context get() = handler.context
|
||||
private val bot get() = context.bot
|
||||
private val logger get() = bot.logger
|
||||
private val account get() = bot.account
|
||||
|
||||
|
||||
private suspend fun fastLogin() {
|
||||
val login10 = WtLogin10(bot.client).sendAndExpect(bot)
|
||||
check(login10 is LoginPacketResponse.Success) { "Fast login failed: $login10" }
|
||||
}
|
||||
|
||||
private fun loginSolverNotNull(): LoginSolver {
|
||||
fun LoginSolver?.notnull(): LoginSolver {
|
||||
checkNotNull(this) {
|
||||
"No LoginSolver found. Please provide by BotConfiguration.loginSolver. " +
|
||||
"For example use `BotFactory.newBot(...) { loginSolver = yourLoginSolver}` in Kotlin, " +
|
||||
"use `BotFactory.newBot(..., new BotConfiguration() {{ setLoginSolver(yourLoginSolver) }})` in Java."
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return bot.configuration.loginSolver.notnull()
|
||||
}
|
||||
|
||||
private val sliderSupported get() = bot.configuration.loginSolver?.isSliderCaptchaSupported ?: false
|
||||
|
||||
private fun createUnsupportedSliderCaptchaException(allowSlider: Boolean): UnsupportedSliderCaptchaException {
|
||||
return UnsupportedSliderCaptchaException(
|
||||
buildString {
|
||||
append("Mirai 无法完成滑块验证.")
|
||||
if (allowSlider) {
|
||||
append(" 使用协议 ")
|
||||
append(configuration.protocol)
|
||||
append(" 强制要求滑块验证, 请更换协议后重试.")
|
||||
}
|
||||
append(" 另请参阅: https://github.com/project-mirai/mirai-login-solver-selenium")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun slowLogin() = withExceptionCollector {
|
||||
|
||||
var allowSlider = sliderSupported || bot.configuration.protocol == MiraiProtocol.ANDROID_PHONE
|
||||
|
||||
var response: LoginPacketResponse = WtLogin9(bot.client, allowSlider).sendAndExpect()
|
||||
|
||||
mainloop@ while (true) {
|
||||
when (response) {
|
||||
is LoginPacketResponse.Success -> {
|
||||
logger.info { "Login successful" }
|
||||
break@mainloop
|
||||
}
|
||||
is LoginPacketResponse.DeviceLockLogin -> {
|
||||
response = WtLogin20(bot.client).sendAndExpect(bot)
|
||||
}
|
||||
|
||||
is LoginPacketResponse.UnsafeLogin -> {
|
||||
loginSolverNotNull().onSolveUnsafeDeviceLoginVerify(bot, response.url)
|
||||
response = WtLogin9(bot.client, allowSlider).sendAndExpect()
|
||||
}
|
||||
|
||||
is Captcha.Picture -> {
|
||||
var result = loginSolverNotNull().onSolvePicCaptcha(bot, response.data)
|
||||
if (result == null || result.length != 4) {
|
||||
//refresh captcha
|
||||
result = "ABCD"
|
||||
}
|
||||
response = WtLogin2.SubmitPictureCaptcha(bot.client, response.sign, result).sendAndExpect()
|
||||
}
|
||||
|
||||
is Captcha.Slider -> {
|
||||
if (sliderSupported) {
|
||||
// use solver
|
||||
val ticket = try {
|
||||
loginSolverNotNull().onSolveSliderCaptcha(bot, response.url)?.takeIf { it.isNotEmpty() }
|
||||
} catch (e: LoginFailedException) {
|
||||
throw e
|
||||
} catch (error: Throwable) {
|
||||
if (allowSlider) {
|
||||
collectException(error)
|
||||
allowSlider = false
|
||||
response = WtLogin9(bot.client, allowSlider).sendAndExpect()
|
||||
continue@mainloop
|
||||
}
|
||||
throw error
|
||||
}
|
||||
response = if (ticket == null) {
|
||||
WtLogin9(bot.client, allowSlider).sendAndExpect()
|
||||
} else {
|
||||
WtLogin2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
|
||||
}
|
||||
} else {
|
||||
// retry once
|
||||
if (!allowSlider) throw createUnsupportedSliderCaptchaException(allowSlider)
|
||||
allowSlider = false
|
||||
response = WtLogin9(bot.client, allowSlider).sendAndExpect()
|
||||
}
|
||||
}
|
||||
|
||||
is LoginPacketResponse.Error -> {
|
||||
if (response.message.contains("0x9a")) { //Error(title=登录失败, message=请你稍后重试。(0x9a), errorInfo=)
|
||||
throw RetryLaterException().initCause(IllegalStateException("Login failed: $response"))
|
||||
}
|
||||
val msg = response.toString()
|
||||
throw WrongPasswordException(buildString(capacity = msg.length) {
|
||||
append(msg)
|
||||
if (msg.contains("当前上网环境异常")) { // Error(title=禁止登录, message=当前上网环境异常,请更换网络环境或在常用设备上登录或稍后再试。, errorInfo=)
|
||||
append(", tips=若频繁出现, 请尝试开启设备锁")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
is LoginPacketResponse.SMSVerifyCodeNeeded -> {
|
||||
val message = "SMS required: $response, which isn't yet supported"
|
||||
logger.error(message)
|
||||
throw UnsupportedSMSLoginException(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal fun initClient() {
|
||||
val device = configuration.deviceInfo?.invoke(bot) ?: DeviceInfo.random()
|
||||
ssoContext.client = QQAndroidClient(
|
||||
bot.account,
|
||||
device = device,
|
||||
accountSecrets = loadSecretsFromCacheOrCreate(device)
|
||||
).apply {
|
||||
_bot = bot
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <R : Packet?> OutgoingPacketWithRespType<R>.sendAndExpect(): R = sendAndExpect(bot)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// cache
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: 2021/4/14 extract a cache service
|
||||
|
||||
private val cacheDir: File by lazy {
|
||||
configuration.workingDir.resolve(bot.configuration.cacheDir).apply { mkdirs() }
|
||||
}
|
||||
private val accountSecretsFile: File by lazy {
|
||||
cacheDir.resolve("account.secrets")
|
||||
}
|
||||
|
||||
private fun loadSecretsFromCacheOrCreate(deviceInfo: DeviceInfo): AccountSecrets {
|
||||
val loaded = if (configuration.loginCacheEnabled && accountSecretsFile.exists()) {
|
||||
kotlin.runCatching {
|
||||
TEA.decrypt(accountSecretsFile.readBytes(), account.passwordMd5).loadAs(AccountSecretsImpl.serializer())
|
||||
}.getOrElse { e ->
|
||||
logger.error("Failed to load account secrets from local cache. Invalidating cache...", e)
|
||||
accountSecretsFile.delete()
|
||||
null
|
||||
}
|
||||
} else null
|
||||
if (loaded != null) {
|
||||
logger.info { "Loaded account secrets from local cache." }
|
||||
return loaded
|
||||
}
|
||||
|
||||
return AccountSecretsImpl(deviceInfo, account) // wLoginSigInfoField is null, no need to save.
|
||||
}
|
||||
|
||||
}
|
@ -30,6 +30,7 @@ internal class OutgoingPacketWithRespType<R : Packet?> constructor(
|
||||
delegate: ByteReadPacket
|
||||
) : OutgoingPacket(name, commandName, sequenceId, delegate)
|
||||
|
||||
// TODO: 2021/4/12 generalize
|
||||
internal open class OutgoingPacket constructor(
|
||||
name: String?,
|
||||
val commandName: String,
|
||||
@ -39,6 +40,30 @@ internal open class OutgoingPacket constructor(
|
||||
val name: String = name ?: commandName
|
||||
}
|
||||
|
||||
internal class IncomingPacket constructor(
|
||||
val commandName: String,
|
||||
val sequenceId: Int,
|
||||
|
||||
val data: Packet?,
|
||||
/**
|
||||
* If not `null`, [data] is `null`
|
||||
*/
|
||||
val exception: Throwable?, // may complete with exception (thrown by decoders)
|
||||
) {
|
||||
init {
|
||||
if (exception != null) require(data == null) { "When exception is not null, data must be null." }
|
||||
if (data != null) require(exception == null) { "When data is not null, exception must be null." }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return if (exception == null) {
|
||||
"IncomingPacket(cmd=$commandName, seq=$sequenceId, SUCCESS, r=$data)"
|
||||
} else {
|
||||
"IncomingPacket(cmd=$commandName, seq=$sequenceId, FAILURE, e=$exception)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend inline fun <E : Packet> OutgoingPacketWithRespType<E>.sendAndExpect(
|
||||
network: QQAndroidBotNetworkHandler,
|
||||
timeoutMillis: Long = 5000,
|
||||
|
@ -96,11 +96,14 @@ internal abstract class IncomingPacketFactory<TPacket : Packet?>(
|
||||
}
|
||||
|
||||
@JvmName("decode0")
|
||||
private suspend inline fun <P : Packet?> OutgoingPacketFactory<P>.decode(bot: QQAndroidBot, packet: ByteReadPacket): P =
|
||||
internal suspend inline fun <P : Packet?> OutgoingPacketFactory<P>.decode(
|
||||
bot: QQAndroidBot,
|
||||
packet: ByteReadPacket
|
||||
): P =
|
||||
packet.decode(bot)
|
||||
|
||||
@JvmName("decode1")
|
||||
private suspend inline fun <P : Packet?> IncomingPacketFactory<P>.decode(
|
||||
internal suspend inline fun <P : Packet?> IncomingPacketFactory<P>.decode(
|
||||
bot: QQAndroidBot,
|
||||
packet: ByteReadPacket,
|
||||
sequenceId: Int
|
||||
|
@ -20,7 +20,7 @@ internal object WtLogin2 : WtLoginExt {
|
||||
fun SubmitSliderCaptcha(
|
||||
client: QQAndroidClient,
|
||||
ticket: String
|
||||
): OutgoingPacket = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
||||
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
||||
writeShort(2) // subCommand
|
||||
@ -37,7 +37,7 @@ internal object WtLogin2 : WtLoginExt {
|
||||
client: QQAndroidClient,
|
||||
captchaSign: ByteArray,
|
||||
captchaAnswer: String
|
||||
): OutgoingPacket = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
||||
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
||||
writeShort(2) // subCommand
|
||||
|
@ -20,7 +20,7 @@ internal object WtLogin9 : WtLoginExt {
|
||||
operator fun invoke(
|
||||
client: QQAndroidClient,
|
||||
allowSlider: Boolean
|
||||
): OutgoingPacket = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||
) = WtLogin.Login.buildLoginOutgoingPacket(client, bodyType = 2) { sequenceId ->
|
||||
writeSsoPacket(client, client.subAppId, WtLogin.Login.commandName, sequenceId = sequenceId) {
|
||||
writeOicqRequestPacket(client, EncryptMethodECDH(client.ecdh), 0x0810) {
|
||||
writeShort(9) // subCommand
|
||||
|
14
mirai-core/src/commonTest/kotlin/network/PacketCodecTest.kt
Normal file
14
mirai-core/src/commonTest/kotlin/network/PacketCodecTest.kt
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network
|
||||
|
||||
internal class PacketCodecTest : AbstractCodecTest()
|
||||
|
||||
internal abstract class AbstractCodecTest
|
60
mirai-core/src/commonTest/kotlin/network/sessionUtils.kt
Normal file
60
mirai-core/src/commonTest/kotlin/network/sessionUtils.kt
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.network
|
||||
|
||||
import net.mamoe.mirai.event.events.BotOnlineEvent
|
||||
import net.mamoe.mirai.internal.QQAndroidBot
|
||||
import net.mamoe.mirai.internal.network.net.protocol.LoginSessionAware
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
|
||||
import net.mamoe.mirai.internal.utils.crypto.ECDH
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.utils.debug
|
||||
import net.mamoe.mirai.utils.withUse
|
||||
import java.io.File
|
||||
|
||||
internal class TestLoginSessionAware(
|
||||
private val accountSecrets: AccountSecrets,
|
||||
override var outgoingPacketSessionId: ByteArray = byteArrayOf(1, 2, 3, 4),
|
||||
override var loginState: Int = 0,
|
||||
override val ecdh: ECDH = ECDH(),
|
||||
) : LoginSessionAware {
|
||||
override var wLoginSigInfo: WLoginSigInfo by accountSecrets::wLoginSigInfo
|
||||
override val randomKey: ByteArray by accountSecrets::randomKey
|
||||
}
|
||||
|
||||
internal fun loadSession(
|
||||
resourceName: String,
|
||||
): AccountSecretsImpl {
|
||||
val bytes = ClassLoader.getSystemResourceAsStream(resourceName)?.withUse { readBytes() }
|
||||
?: error("AccountSecrets resource '$resourceName' not found.")
|
||||
return bytes.loadAs(AccountSecretsImpl.serializer())
|
||||
}
|
||||
|
||||
/**
|
||||
* secure to share with others.
|
||||
*/
|
||||
internal fun QQAndroidClient.dumpSessionSafe(): ByteArray {
|
||||
val secrets =
|
||||
AccountSecretsImpl(device, account).copy(
|
||||
wLoginSigInfoField = wLoginSigInfo.copy(
|
||||
tgt = EMPTY_BYTE_ARRAY,
|
||||
encryptA1 = EMPTY_BYTE_ARRAY,
|
||||
)
|
||||
)
|
||||
return secrets.toByteArray(AccountSecretsImpl.serializer())
|
||||
}
|
||||
|
||||
internal fun QQAndroidBot.scheduleSafeSessionDump(outputFile: File) {
|
||||
this.eventChannel.subscribeAlways<BotOnlineEvent> {
|
||||
outputFile.writeBytes(client.dumpSessionSafe())
|
||||
bot.logger.debug { "Dumped safe session to " }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user