2022-06-30 16:32:56 +03:00
|
|
|
import Long from 'long'
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
import { mtp, tl } from '@mtcute/tl'
|
2023-09-24 01:32:22 +03:00
|
|
|
import { TlBinaryWriter, TlReaderMap, TlSerializationCounter, TlWriterMap } from '@mtcute/tl-runtime'
|
2022-06-30 16:32:56 +03:00
|
|
|
|
2023-10-16 19:23:53 +03:00
|
|
|
import { MtcuteError } from '../types/index.js'
|
2023-08-23 22:11:42 +03:00
|
|
|
import {
|
2023-12-11 00:07:41 +03:00
|
|
|
compareLongs,
|
2023-08-23 22:11:42 +03:00
|
|
|
ControllablePromise,
|
2023-06-10 00:37:26 +03:00
|
|
|
Deque,
|
|
|
|
getRandomInt,
|
2022-11-05 03:03:21 +03:00
|
|
|
ICryptoProvider,
|
|
|
|
Logger,
|
2023-06-10 00:37:26 +03:00
|
|
|
LongMap,
|
2022-11-05 03:03:21 +03:00
|
|
|
LruSet,
|
2023-06-10 00:37:26 +03:00
|
|
|
randomLong,
|
2022-11-05 03:03:21 +03:00
|
|
|
SortedArray,
|
2023-10-16 19:23:53 +03:00
|
|
|
} from '../utils/index.js'
|
|
|
|
import { AuthKey } from './auth-key.js'
|
2023-12-11 06:15:31 +03:00
|
|
|
import { ServerSaltManager } from './server-salt.js'
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
export interface PendingRpc {
|
|
|
|
method: string
|
2023-10-16 19:23:53 +03:00
|
|
|
data: Uint8Array
|
2022-11-05 03:03:21 +03:00
|
|
|
promise: ControllablePromise
|
|
|
|
stack?: string
|
|
|
|
gzipOverhead?: number
|
|
|
|
|
2023-12-11 00:07:41 +03:00
|
|
|
chainId?: string | number
|
|
|
|
invokeAfter?: Long
|
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
sent?: boolean
|
2023-10-05 18:10:15 +03:00
|
|
|
done?: boolean
|
2022-11-05 03:03:21 +03:00
|
|
|
msgId?: Long
|
|
|
|
seqNo?: number
|
|
|
|
containerId?: Long
|
|
|
|
acked?: boolean
|
|
|
|
initConn?: boolean
|
|
|
|
getState?: number
|
|
|
|
cancelled?: boolean
|
2023-06-10 00:37:26 +03:00
|
|
|
timeout?: NodeJS.Timeout
|
2022-11-05 03:03:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
export type PendingMessage =
|
|
|
|
| {
|
|
|
|
_: 'rpc'
|
|
|
|
rpc: PendingRpc
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
_: 'container'
|
|
|
|
msgIds: Long[]
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
_: 'state'
|
|
|
|
msgIds: Long[]
|
|
|
|
containerId: Long
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
_: 'resend'
|
|
|
|
msgIds: Long[]
|
|
|
|
containerId: Long
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
_: 'ping'
|
|
|
|
pingId: Long
|
|
|
|
containerId: Long
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
_: 'destroy_session'
|
|
|
|
sessionId: Long
|
|
|
|
containerId: Long
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
_: 'cancel'
|
|
|
|
msgId: Long
|
|
|
|
containerId: Long
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
_: 'future_salts'
|
|
|
|
containerId: Long
|
|
|
|
}
|
2022-11-07 00:08:59 +03:00
|
|
|
| {
|
|
|
|
_: 'bind'
|
|
|
|
promise: ControllablePromise
|
|
|
|
}
|
2022-11-05 03:03:21 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
/**
|
2022-11-06 02:27:46 +03:00
|
|
|
* Class encapsulating a single MTProto session and storing
|
|
|
|
* all the relevant state
|
2021-04-08 12:19:38 +03:00
|
|
|
*/
|
|
|
|
export class MtprotoSession {
|
2021-11-23 00:03:59 +03:00
|
|
|
_sessionId = randomLong()
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2022-11-06 02:27:46 +03:00
|
|
|
_authKey = new AuthKey(this._crypto, this.log, this._readerMap)
|
2022-11-07 00:08:59 +03:00
|
|
|
_authKeyTemp = new AuthKey(this._crypto, this.log, this._readerMap)
|
|
|
|
_authKeyTempSecondary = new AuthKey(this._crypto, this.log, this._readerMap)
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
_timeOffset = 0
|
|
|
|
_lastMessageId = Long.ZERO
|
|
|
|
_seqNo = 0
|
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
/// state ///
|
|
|
|
// recent msg ids
|
2023-09-19 01:33:47 +03:00
|
|
|
recentOutgoingMsgIds = new LruSet<Long>(1000, true)
|
|
|
|
recentIncomingMsgIds = new LruSet<Long>(1000, true)
|
2022-11-05 03:03:21 +03:00
|
|
|
|
|
|
|
// queues
|
|
|
|
queuedRpc = new Deque<PendingRpc>()
|
|
|
|
queuedAcks: Long[] = []
|
|
|
|
queuedStateReq: Long[] = []
|
|
|
|
queuedResendReq: Long[] = []
|
|
|
|
queuedCancelReq: Long[] = []
|
2023-09-24 01:32:22 +03:00
|
|
|
getStateSchedule = new SortedArray<PendingRpc>([], (a, b) => a.getState! - b.getState!)
|
2022-11-05 03:03:21 +03:00
|
|
|
|
2023-12-11 00:07:41 +03:00
|
|
|
chains = new Map<string | number, Long>()
|
|
|
|
chainsPendingFails = new Map<string | number, SortedArray<PendingRpc>>()
|
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
// requests info
|
|
|
|
pendingMessages = new LongMap<PendingMessage>()
|
2022-11-07 00:08:59 +03:00
|
|
|
destroySessionIdToMsgId = new LongMap<Long>()
|
2022-11-05 03:03:21 +03:00
|
|
|
|
2023-08-23 22:11:42 +03:00
|
|
|
lastPingRtt = NaN
|
|
|
|
lastPingTime = 0
|
|
|
|
lastPingMsgId = Long.ZERO
|
|
|
|
lastSessionCreatedUid = Long.ZERO
|
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
initConnectionCalled = false
|
2022-11-14 00:37:57 +03:00
|
|
|
authorizationPending = false
|
2022-11-05 03:03:21 +03:00
|
|
|
|
2023-08-23 22:11:42 +03:00
|
|
|
next429Timeout = 1000
|
|
|
|
current429Timeout?: NodeJS.Timeout
|
|
|
|
next429ResetTimeout?: NodeJS.Timeout
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
constructor(
|
2022-11-06 02:27:46 +03:00
|
|
|
readonly _crypto: ICryptoProvider,
|
2021-11-23 00:03:59 +03:00
|
|
|
readonly log: Logger,
|
|
|
|
readonly _readerMap: TlReaderMap,
|
2023-06-05 03:30:48 +03:00
|
|
|
readonly _writerMap: TlWriterMap,
|
2023-12-11 06:15:31 +03:00
|
|
|
readonly _salts: ServerSaltManager,
|
2021-11-23 00:03:59 +03:00
|
|
|
) {
|
2022-11-05 03:03:21 +03:00
|
|
|
this.log.prefix = `[SESSION ${this._sessionId.toString(16)}] `
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2023-08-23 22:11:42 +03:00
|
|
|
get hasPendingMessages(): boolean {
|
|
|
|
return Boolean(
|
|
|
|
this.queuedRpc.length ||
|
|
|
|
this.queuedAcks.length ||
|
|
|
|
this.queuedStateReq.length ||
|
|
|
|
this.queuedResendReq.length,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
/**
|
2022-11-06 02:27:46 +03:00
|
|
|
* Reset session by resetting auth key(s) and session state
|
2022-11-05 03:03:21 +03:00
|
|
|
*/
|
2022-11-14 00:37:57 +03:00
|
|
|
reset(withAuthKey = false): void {
|
|
|
|
if (withAuthKey) {
|
|
|
|
this._authKey.reset()
|
|
|
|
this._authKeyTemp.reset()
|
|
|
|
this._authKeyTempSecondary.reset()
|
|
|
|
}
|
2022-11-05 03:03:21 +03:00
|
|
|
|
2023-08-23 22:11:42 +03:00
|
|
|
clearTimeout(this.current429Timeout)
|
2022-11-05 03:03:21 +03:00
|
|
|
this.resetState()
|
2023-08-23 22:11:42 +03:00
|
|
|
this.resetLastPing(true)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
/**
|
|
|
|
* Reset session state and generate a new session ID.
|
|
|
|
*
|
|
|
|
* By default, also cancels any pending RPC requests.
|
|
|
|
* If `keepPending` is set to `true`, pending requests will be kept
|
|
|
|
*/
|
|
|
|
resetState(keepPending = false): void {
|
|
|
|
this._lastMessageId = Long.ZERO
|
2021-11-23 00:03:59 +03:00
|
|
|
this._seqNo = 0
|
2022-11-05 03:03:21 +03:00
|
|
|
|
|
|
|
this._sessionId = randomLong()
|
2022-11-07 00:08:59 +03:00
|
|
|
this.log.debug('session reset, new sid = %h', this._sessionId)
|
2022-11-05 03:03:21 +03:00
|
|
|
this.log.prefix = `[SESSION ${this._sessionId.toString(16)}] `
|
|
|
|
|
|
|
|
// reset session state
|
|
|
|
|
|
|
|
if (!keepPending) {
|
|
|
|
for (const info of this.pendingMessages.values()) {
|
|
|
|
if (info._ === 'rpc') {
|
2023-09-22 15:32:28 +03:00
|
|
|
info.rpc.promise.reject(new MtcuteError('Session is reset'))
|
2022-11-05 03:03:21 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this.pendingMessages.clear()
|
|
|
|
}
|
|
|
|
|
|
|
|
this.recentOutgoingMsgIds.clear()
|
|
|
|
this.recentIncomingMsgIds.clear()
|
|
|
|
|
|
|
|
if (!keepPending) {
|
|
|
|
while (this.queuedRpc.length) {
|
|
|
|
const rpc = this.queuedRpc.popFront()!
|
|
|
|
|
|
|
|
if (rpc.sent === false) {
|
2023-09-22 15:32:28 +03:00
|
|
|
rpc.promise.reject(new MtcuteError('Session is reset'))
|
2022-11-05 03:03:21 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.queuedAcks.length = 0
|
|
|
|
this.queuedStateReq.length = 0
|
|
|
|
this.queuedResendReq.length = 0
|
|
|
|
this.getStateSchedule.clear()
|
2023-12-11 00:07:41 +03:00
|
|
|
this.chains.clear()
|
2022-11-05 03:03:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
enqueueRpc(rpc: PendingRpc, force?: boolean): boolean {
|
|
|
|
// already queued or cancelled
|
|
|
|
if ((!force && !rpc.sent) || rpc.cancelled) return false
|
|
|
|
|
|
|
|
rpc.sent = false
|
|
|
|
rpc.containerId = undefined
|
2023-09-24 01:32:22 +03:00
|
|
|
this.log.debug('enqueued %s for sending (msg_id = %s)', rpc.method, rpc.msgId || 'n/a')
|
2022-11-05 03:03:21 +03:00
|
|
|
this.queuedRpc.pushBack(rpc)
|
2023-06-10 00:37:26 +03:00
|
|
|
|
2022-11-05 03:03:21 +03:00
|
|
|
return true
|
2021-11-23 00:03:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
getMessageId(): Long {
|
|
|
|
const timeTicks = Date.now()
|
|
|
|
const timeSec = Math.floor(timeTicks / 1000) + this._timeOffset
|
|
|
|
const timeMSec = timeTicks % 1000
|
|
|
|
const random = getRandomInt(0xffff)
|
|
|
|
|
|
|
|
let messageId = new Long((timeMSec << 21) | (random << 3) | 4, timeSec)
|
|
|
|
|
|
|
|
if (this._lastMessageId.gt(messageId)) {
|
|
|
|
messageId = this._lastMessageId.add(4)
|
|
|
|
}
|
|
|
|
|
|
|
|
this._lastMessageId = messageId
|
|
|
|
|
|
|
|
return messageId
|
|
|
|
}
|
|
|
|
|
|
|
|
getSeqNo(isContentRelated = true): number {
|
2023-08-23 22:11:42 +03:00
|
|
|
let seqNo = this._seqNo
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
if (isContentRelated) {
|
|
|
|
seqNo += 1
|
2023-08-23 22:11:42 +03:00
|
|
|
this._seqNo += 2
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
return seqNo
|
|
|
|
}
|
|
|
|
|
2022-11-07 00:08:59 +03:00
|
|
|
/** Encrypt a single MTProto message using session's keys */
|
2023-11-07 22:49:35 +03:00
|
|
|
encryptMessage(message: Uint8Array): Uint8Array {
|
2022-11-07 00:08:59 +03:00
|
|
|
const key = this._authKeyTemp.ready ? this._authKeyTemp : this._authKey
|
2023-06-10 00:37:26 +03:00
|
|
|
|
2023-12-11 06:15:31 +03:00
|
|
|
return key.encryptMessage(message, this._salts.currentSalt, this._sessionId)
|
2022-11-07 00:08:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Decrypt a single MTProto message using session's keys */
|
2023-11-07 22:49:35 +03:00
|
|
|
decryptMessage(data: Uint8Array, callback: Parameters<AuthKey['decryptMessage']>[2]): void {
|
2023-09-22 15:32:28 +03:00
|
|
|
if (!this._authKey.ready) throw new MtcuteError('Keys are not set up!')
|
2022-11-07 00:08:59 +03:00
|
|
|
|
2023-10-16 19:23:53 +03:00
|
|
|
const authKeyId = data.subarray(0, 8)
|
2022-11-07 00:08:59 +03:00
|
|
|
|
|
|
|
let key: AuthKey
|
2023-06-10 00:37:26 +03:00
|
|
|
|
2022-11-07 00:08:59 +03:00
|
|
|
if (this._authKey.match(authKeyId)) {
|
|
|
|
key = this._authKey
|
|
|
|
} else if (this._authKeyTemp.match(authKeyId)) {
|
|
|
|
key = this._authKeyTemp
|
|
|
|
} else if (this._authKeyTempSecondary.match(authKeyId)) {
|
|
|
|
key = this._authKeyTempSecondary
|
|
|
|
} else {
|
|
|
|
this.log.warn(
|
|
|
|
'received message with unknown authKey = %h (expected %h or %h or %h)',
|
|
|
|
authKeyId,
|
|
|
|
this._authKey.id,
|
|
|
|
this._authKeyTemp.id,
|
|
|
|
this._authKeyTempSecondary.id,
|
|
|
|
)
|
2023-06-10 00:37:26 +03:00
|
|
|
|
2022-11-07 00:08:59 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
return key.decryptMessage(data, this._sessionId, callback)
|
|
|
|
}
|
|
|
|
|
2023-10-16 19:23:53 +03:00
|
|
|
writeMessage(
|
|
|
|
writer: TlBinaryWriter,
|
|
|
|
content: tl.TlObject | mtp.TlObject | Uint8Array,
|
|
|
|
isContentRelated = true,
|
|
|
|
): Long {
|
2021-11-23 00:03:59 +03:00
|
|
|
const messageId = this.getMessageId()
|
|
|
|
const seqNo = this.getSeqNo(isContentRelated)
|
|
|
|
|
2023-10-16 19:23:53 +03:00
|
|
|
const length = ArrayBuffer.isView(content) ?
|
2023-06-05 03:30:48 +03:00
|
|
|
content.length :
|
2023-09-24 01:32:22 +03:00
|
|
|
TlSerializationCounter.countNeededBytes(writer.objectMap!, content)
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
writer.long(messageId)
|
|
|
|
writer.int(seqNo)
|
|
|
|
writer.uint(length)
|
2023-10-16 19:23:53 +03:00
|
|
|
if (ArrayBuffer.isView(content)) writer.raw(content)
|
2021-11-23 00:03:59 +03:00
|
|
|
else writer.object(content as tl.TlObject)
|
|
|
|
|
|
|
|
return messageId
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
2023-08-23 22:11:42 +03:00
|
|
|
|
|
|
|
onTransportFlood(callback: () => void) {
|
|
|
|
if (this.current429Timeout) return // already waiting
|
|
|
|
|
|
|
|
// all active queries must be resent after a timeout
|
|
|
|
this.resetLastPing(true)
|
|
|
|
|
|
|
|
const timeout = this.next429Timeout
|
|
|
|
|
|
|
|
this.next429Timeout = Math.min(this.next429Timeout * 2, 32000)
|
|
|
|
clearTimeout(this.current429Timeout)
|
|
|
|
clearTimeout(this.next429ResetTimeout)
|
|
|
|
|
|
|
|
this.current429Timeout = setTimeout(() => {
|
|
|
|
this.current429Timeout = undefined
|
|
|
|
callback()
|
|
|
|
}, timeout)
|
|
|
|
this.next429ResetTimeout = setTimeout(() => {
|
|
|
|
this.next429ResetTimeout = undefined
|
|
|
|
this.next429Timeout = 1000
|
|
|
|
}, 60000)
|
|
|
|
|
2023-09-24 01:32:22 +03:00
|
|
|
this.log.debug('transport flood, waiting for %d ms before proceeding', timeout)
|
2023-08-23 22:11:42 +03:00
|
|
|
|
|
|
|
return Date.now() + timeout
|
|
|
|
}
|
|
|
|
|
|
|
|
resetLastPing(withTime = false): void {
|
|
|
|
if (withTime) this.lastPingTime = 0
|
|
|
|
|
|
|
|
if (!this.lastPingMsgId.isZero()) {
|
|
|
|
this.pendingMessages.delete(this.lastPingMsgId)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.lastPingMsgId = Long.ZERO
|
|
|
|
}
|
2023-12-11 00:07:41 +03:00
|
|
|
|
|
|
|
addToChain(chainId: string | number, msgId: Long): Long | undefined {
|
|
|
|
const prevMsgId = this.chains.get(chainId)
|
|
|
|
this.chains.set(chainId, msgId)
|
|
|
|
|
|
|
|
this.log.debug('added message %l to chain %s (prev: %l)', msgId, chainId, prevMsgId)
|
|
|
|
|
|
|
|
return prevMsgId
|
|
|
|
}
|
|
|
|
|
|
|
|
removeFromChain(chainId: string | number, msgId: Long): void {
|
|
|
|
const lastMsgId = this.chains.get(chainId)
|
|
|
|
|
|
|
|
if (!lastMsgId) {
|
|
|
|
this.log.warn('tried to remove message %l from empty chain %s', msgId, chainId)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lastMsgId.eq(msgId)) {
|
|
|
|
// last message of the chain, remove it
|
|
|
|
this.log.debug('chain %s: exhausted, last message %l', msgId, chainId)
|
|
|
|
this.chains.delete(chainId)
|
|
|
|
}
|
|
|
|
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
getPendingChainedFails(chainId: string | number): SortedArray<PendingRpc> {
|
|
|
|
let arr = this.chainsPendingFails.get(chainId)
|
|
|
|
|
|
|
|
if (!arr) {
|
|
|
|
arr = new SortedArray<PendingRpc>([], (a, b) => compareLongs(a.invokeAfter!, b.invokeAfter!))
|
|
|
|
this.chainsPendingFails.set(chainId, arr)
|
|
|
|
}
|
|
|
|
|
|
|
|
return arr
|
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|