New network: framework infrastructure

This commit is contained in:
Him188 2021-04-13 00:23:27 +08:00
parent 48564056df
commit b844efb072
21 changed files with 1241 additions and 48 deletions

View File

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

View File

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

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

View File

@ -72,6 +72,7 @@ kotlin {
api1(`kotlinx-io-jvm`)
implementation1(`kotlinx-coroutines-io`)
implementation(`netty-all`)
}
}

View File

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

View File

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

View File

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

View File

@ -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]
/**

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

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