tweb/src/lib/mtproto/networker.ts

1219 lines
38 KiB
TypeScript

import {isObject} from '../bin_utils';
import {convertToUint8Array,
bufferConcat, nextRandomInt, bytesToHex, longToBytes,
bytesCmp, bigStringInt} from '../bin_utils';
import {TLDeserialization, TLSerialization} from './tl_utils';
import CryptoWorker from '../crypto/cryptoworker';
import AppStorage from '../storage';
import Schema from './schema';
import timeManager from './timeManager';
import NetworkerFactory from './networkerFactory';
import dcConfigurator from './dcConfigurator';
import Socket from './transports/websocket';
import HTTP from './transports/http';
import { logger, LogLevels } from '../polyfill';
import { Modes, App } from './mtproto_config';
import { InvokeApiOptions } from '../../types';
//console.error('networker included!', new Error().stack);
type Message = {
msg_id: string,
seq_no: number,
body?: Uint8Array | number[],
isAPI?: boolean,
acked?: boolean,
deferred?: {
resolve: any,
reject: any
},
container?: boolean,
inner?: string[],
notContentRelated?: boolean,
noSchedule?: boolean,
resultType?: string,
singleInRequest?: boolean,
longPoll?: boolean,
noResponse?: boolean, // only with http (http_wait for longPoll)
};
class MTPNetworker {
private authKeyUint8: Uint8Array;
private upload: boolean;
private lastServerMessages: Array<string> = [];
private sentMessages: {
[msgID: string]: Message
} = {};
private pendingMessages: any = {};
private pendingAcks: Array<string> = [];
private pendingResends: Array<string> = [];
private connectionInited = false;
//private longPollInt: number;
private longPollPending = 0;
private seqNo: number = 0;
private prevSessionID: Array<number> = [];
private sessionID: Array<number> = [];
private sleepAfter = 0;
private offline = false;
private checkConnectionTimeout: number;
private checkConnectionPeriod = 0;
private nextReqTimeout: number;
private nextReq: number = 0;
private onOnlineCb = this.checkConnection.bind(this);
private lastResendReq: {
req_msg_id: string,
resend_msg_ids: Array<string>
} | null = null;
private transport: Socket | HTTP;
private log: ReturnType<typeof logger>;
constructor(private dcID: number, private authKey: number[], private authKeyID: Uint8Array,
private serverSalt: number[], private options: InvokeApiOptions = {}) {
this.authKeyUint8 = convertToUint8Array(this.authKey);
//this.authKeyID = sha1BytesSync(this.authKey).slice(-8);
this.upload = this.options.fileUpload || this.options.fileDownload || false;
this.log = logger('NET-' + dcID + (this.upload ? '-U' : ''));
this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */);
/* // Test resend after bad_server_salt
if(this.dcID == 1 && this.upload) {
this.serverSalt[0] = 0;
} */
this.updateSession();
// if(!NetworkerFactory.offlineInited) {
// NetworkerFactory.offlineInited = true;
// /* $rootScope.offline = true
// $rootScope.offlineConnecting = true */
// }
this.transport = dcConfigurator.chooseServer(this.dcID, this.upload);
if(this.transport instanceof HTTP) {
/* this.longPollInt = */setInterval(this.checkLongPoll.bind(this), 10000);
this.checkLongPoll();
} else {
(this.transport as Socket).networker = this;
}
}
public updateSession() {
this.seqNo = 0;
this.prevSessionID = this.sessionID;
this.sessionID = new Array(8);
this.sessionID = [...new Uint8Array(this.sessionID.length).randomize()];
//MTProto.secureRandom.nextBytes(this.sessionID);
}
public updateSentMessage(sentMessageID: any) {
var sentMessage = this.sentMessages[sentMessageID];
if(!sentMessage) {
return false;
}
var self = this;
if(sentMessage.container) {
var newInner: any = [];
sentMessage.inner.forEach((innerSentMessageID: any) => {
var innerSentMessage = self.updateSentMessage(innerSentMessageID);
if(innerSentMessage) {
newInner.push(innerSentMessage.msg_id);
}
})
sentMessage.inner = newInner;
}
sentMessage.msg_id = timeManager.generateID();
sentMessage.seq_no = this.generateSeqNo(
sentMessage.notContentRelated ||
sentMessage.container
);
this.sentMessages[sentMessage.msg_id] = sentMessage;
delete self.sentMessages[sentMessageID];
return sentMessage;
}
public generateSeqNo(notContentRelated?: boolean) {
var seqNo = this.seqNo * 2;
if(!notContentRelated) {
seqNo++;
this.seqNo++;
}
return seqNo;
}
public wrapMtpCall(method: string, params: any = {}, options: any = {}) {
var serializer = new TLSerialization({mtproto: true});
serializer.storeMethod(method, params);
var messageID = timeManager.generateID();
var seqNo = this.generateSeqNo();
var message = {
msg_id: messageID,
seq_no: seqNo,
body: serializer.getBytes()
};
if(Modes.debug) {
this.log('MT call', method, params, messageID, seqNo);
}
return this.pushMessage(message, options);
}
public wrapMtpMessage(object: any = {}, options: any = {}) {
var serializer = new TLSerialization({mtproto: true});
serializer.storeObject(object, 'Object');
var messageID = timeManager.generateID();
var seqNo = this.generateSeqNo(options.notContentRelated);
var message = {
msg_id: messageID,
seq_no: seqNo,
body: serializer.getBytes()
};
if(Modes.debug) {
this.log('MT message', object, messageID, seqNo);
}
return this.pushMessage(message, options);
}
public wrapApiCall(method: string, params: any = {}, options: InvokeApiOptions = {}) {
let serializer = new TLSerialization(options);
if(!this.connectionInited) { // this will call once for each new session
///////this.log('Wrap api call !this.connectionInited');
let invokeWithLayer = Schema.API.methods.find(m => m.method == 'invokeWithLayer');
if(!invokeWithLayer) throw new Error('no invokeWithLayer!');
serializer.storeInt(+invokeWithLayer.id >>> 0, 'invokeWithLayer');
// @ts-ignore
serializer.storeInt(Schema.layer, 'layer');
let initConnection = Schema.API.methods.find(m => m.method == 'initConnection');
if(!initConnection) throw new Error('no initConnection!');
serializer.storeInt(+initConnection.id >>> 0, 'initConnection');
serializer.storeInt(0x0, 'flags');
serializer.storeInt(App.id, 'api_id');
serializer.storeString(navigator.userAgent || 'Unknown UserAgent', 'device_model');
serializer.storeString(navigator.platform || 'Unknown Platform', 'system_version');
serializer.storeString(App.version, 'app_version');
serializer.storeString(navigator.language || 'en', 'system_lang_code');
serializer.storeString('', 'lang_pack');
serializer.storeString(navigator.language || 'en', 'lang_code');
//serializer.storeInt(0x0, 'proxy');
/* serializer.storeMethod('initConnection', {
'flags': 0,
'api_id': App.id,
'device_model': navigator.userAgent || 'Unknown UserAgent',
'system_version': navigator.platform || 'Unknown Platform',
'app_version': App.version,
'system_lang_code': navigator.language || 'en',
'lang_pack': '',
'lang_code': navigator.language || 'en'
}); */
}
if(options.afterMessageID) {
let invokeAfterMsg = Schema.API.methods.find(m => m.method == 'invokeAfterMsg');
if(!invokeAfterMsg) throw new Error('no invokeAfterMsg!');
this.log('Api call options.afterMessageID!');
serializer.storeInt(+invokeAfterMsg.id >>> 0, 'invokeAfterMsg');
serializer.storeLong(options.afterMessageID, 'msg_id');
}
options.resultType = serializer.storeMethod(method, params);
/* if(method == 'account.updateNotifySettings') {
this.log('api call body:', serializer.getBytes(true));
} */
var messageID = timeManager.generateID();
var seqNo = this.generateSeqNo();
var message = {
msg_id: messageID,
seq_no: seqNo,
body: serializer.getBytes(true),
isAPI: true
};
if(Modes.debug/* || true */) {
this.log('Api call', method, message, params, options);
} else {
this.log('Api call', method, params, options);
}
return this.pushMessage(message, options);
}
public checkLongPoll() {
const isClean = this.cleanupSent();
//this.log('Check lp', this.longPollPending, tsNow(), this.dcID, isClean, this);
if((this.longPollPending && Date.now() < this.longPollPending) ||
this.offline) {
//this.log('No lp this time');
return false;
}
AppStorage.get<number>('dc').then((baseDcID: number) => {
if(isClean && (
baseDcID != this.dcID ||
this.upload ||
(this.sleepAfter && Date.now() > this.sleepAfter)
)) {
//console.warn(dT(), 'Send long-poll for DC is delayed', this.dcID, this.sleepAfter);
return;
}
this.sendLongPoll();
});
}
public sendLongPoll() {
let maxWait = 25000;
this.longPollPending = Date.now() + maxWait;
//this.log('Set lp', this.longPollPending, tsNow())
this.wrapMtpCall('http_wait', {
max_delay: 500,
wait_after: 150,
max_wait: maxWait
}, {
noResponse: true,
longPoll: true
}).then(() => {
this.longPollPending = 0;
setTimeout(this.checkLongPoll.bind(this), 0);
}, (error: ErrorEvent) => {
this.log('Long-poll failed', error);
});
}
// тут можно сделать таймаут и выводить дисконнект
public pushMessage(message: {
msg_id: string,
seq_no: number,
body: Uint8Array | number[],
isAPI?: boolean
}, options: any = {}) {
return new Promise((resolve, reject) => {
this.sentMessages[message.msg_id] = Object.assign(message, options, {
deferred: {resolve, reject}
});
// this.log('Networker pushMessage:', this.sentMessages[message.msg_id]);
this.pendingMessages[message.msg_id] = 0;
if(!options || !options.noSchedule) {
this.scheduleRequest();
}
if(isObject(options)) {
options.messageID = message.msg_id;
}
});
}
public pushResend(messageID: string, delay = 0) {
const value = delay ? Date.now() + delay : 0;
const sentMessage = this.sentMessages[messageID];
if(sentMessage.container) {
for(let i = 0, length = sentMessage.inner.length; i < length; i++) {
this.pendingMessages[sentMessage.inner[i]] = value;
}
} else {
this.pendingMessages[messageID] = value;
}
this.log('Resend due', messageID, this.pendingMessages);
this.scheduleRequest(delay);
}
public async getMsgKey(dataWithPadding: ArrayBuffer, isOut: boolean) {
const authKey = this.authKeyUint8;
const x = isOut ? 0 : 8
const msgKeyLargePlain = bufferConcat(authKey.subarray(88 + x, 88 + x + 32), dataWithPadding);
const msgKeyLarge = await CryptoWorker.sha256Hash(msgKeyLargePlain);
const msgKey = new Uint8Array(msgKeyLarge).subarray(8, 24);
return msgKey;
};
public getAesKeyIv(msgKey: Uint8Array | number[], isOut: boolean): Promise<[Uint8Array, Uint8Array]> {
var authKey = this.authKeyUint8;
var x = isOut ? 0 : 8;
var sha2aText = new Uint8Array(52);
var sha2bText = new Uint8Array(52);
var promises: Array<Promise<number[]>> = [];
sha2aText.set(msgKey, 0);
sha2aText.set(authKey.subarray(x, x + 36), 16);
promises.push(CryptoWorker.sha256Hash(sha2aText));
sha2bText.set(authKey.subarray(40 + x, 40 + x + 36), 0);
sha2bText.set(msgKey, 36);
promises.push(CryptoWorker.sha256Hash(sha2bText));
return Promise.all(promises).then((results) => {
var aesKey = new Uint8Array(32);
var aesIv = new Uint8Array(32);
var sha2a = new Uint8Array(results[0]);
var sha2b = new Uint8Array(results[1]);
aesKey.set(sha2a.subarray(0, 8));
aesKey.set(sha2b.subarray(8, 24), 8);
aesKey.set(sha2a.subarray(24, 32), 24);
aesIv.set(sha2b.subarray(0, 8));
aesIv.set(sha2a.subarray(8, 24), 8);
aesIv.set(sha2b.subarray(24, 32), 24);
return [aesKey, aesIv];
});
}
public checkConnection(event: Event | string) {
/* $rootScope.offlineConnecting = true */
this.log('Check connection', event);
clearTimeout(this.checkConnectionTimeout);
this.checkConnectionTimeout = 0;
var serializer = new TLSerialization({mtproto: true});
var pingID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];
serializer.storeMethod('ping', {
ping_id: pingID
});
var pingMessage = {
msg_id: timeManager.generateID(),
seq_no: this.generateSeqNo(true),
body: serializer.getBytes()
};
var self = this;
this.sendEncryptedRequest(pingMessage, {
timeout: 15000
}).then((result) => {
/* delete $rootScope.offlineConnecting */
self.toggleOffline(false);
}, () => {
this.log('Delay ', self.checkConnectionPeriod * 1000);
self.checkConnectionTimeout = setTimeout(self.checkConnection.bind(self), self.checkConnectionPeriod * 1000 | 0);
self.checkConnectionPeriod = Math.min(60, self.checkConnectionPeriod * 1.5);
/* setTimeout(function() {
delete $rootScope.offlineConnecting
}, 1000); */
});
}
public toggleOffline(enabled: boolean) {
// this.log('toggle ', enabled, this.dcID, this.iii)
if(this.offline !== undefined && this.offline == enabled) {
return false;
}
this.offline = enabled;
/* $rootScope.offline = enabled;
$rootScope.offlineConnecting = false; */
if(!(this.transport instanceof HTTP)) {
this.log('toggle ', enabled, this.dcID);
return;
}
if(this.offline) {
clearTimeout(this.nextReqTimeout);
this.nextReqTimeout = 0;
this.nextReq = 0;
if(this.checkConnectionPeriod < 1.5) {
this.checkConnectionPeriod = 0;
}
this.checkConnectionTimeout = setTimeout(this.checkConnection.bind(this), this.checkConnectionPeriod * 1000 | 0);
this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5);
document.body.addEventListener('online', this.onOnlineCb, false);
document.body.addEventListener('focus', this.onOnlineCb, false);
} else {
this.checkLongPoll();
this.scheduleRequest();
document.body.removeEventListener('online', this.onOnlineCb);
document.body.removeEventListener('focus', this.onOnlineCb);
clearTimeout(this.checkConnectionTimeout);
this.checkConnectionTimeout = 0;
}
}
public performScheduledRequest() {
// this.log('scheduled', this.dcID, this.iii)
if(this.offline) {
this.log('Cancel scheduled');
return false;
}
this.nextReq = 0;
if(this.pendingAcks.length) {
var ackMsgIDs: Array<string> = this.pendingAcks.slice();
/* for(var i = 0; i < this.pendingAcks.length; i++) {
ackMsgIDs.push(this.pendingAcks[i]);
} */
// this.log('acking messages', ackMsgIDs)
this.wrapMtpMessage({
_: 'msgs_ack',
msg_ids: ackMsgIDs
}, {
notContentRelated: true,
noSchedule: true
});
}
if(this.pendingResends.length) {
var resendMsgIDs: Array<string> = this.pendingResends.slice();
var resendOpts = {
noSchedule: true,
notContentRelated: true,
messageID: '' // will set in wrapMtpMessage->pushMessage
};
/* for(var i = 0; i < this.pendingResends.length; i++) {
resendMsgIDs.push(this.pendingResends[i]);
} */
this.log('resendReq messages', resendMsgIDs);
this.wrapMtpMessage({
_: 'msg_resend_req',
msg_ids: resendMsgIDs
}, resendOpts);
this.lastResendReq = {
req_msg_id: resendOpts.messageID,
resend_msg_ids: resendMsgIDs
};
}
var messages: Message[] = [],
message: Message;
var messagesByteLen = 0;
var currentTime = Date.now();
var hasApiCall = false;
var hasHttpWait = false;
var lengthOverflow = false;
var singlesCount = 0;
var self = this;
for(let messageID in this.pendingMessages) {
let value = this.pendingMessages[messageID];
if(!value || value >= currentTime) {
if(message = this.sentMessages[messageID]) {
//this.log('performScheduledRequest message:', message);
var messageByteLength = (/* message.body.byteLength || */message.body.length) + 32;
if(!message.notContentRelated &&
lengthOverflow) {
continue; // maybe break here
}
if(!message.notContentRelated &&
messagesByteLen &&
messagesByteLen + messageByteLength > 655360) { // 640 Kb
this.log.warn('lengthOverflow', message);
lengthOverflow = true;
continue; // maybe break here
}
if(message.singleInRequest) {
singlesCount++;
if(singlesCount > 1) {
continue; // maybe break here
}
}
messages.push(message);
messagesByteLen += messageByteLength;
if(message.isAPI) {
hasApiCall = true;
} else if(message.longPoll) {
hasHttpWait = true;
}
} else {
// this.log(message, messageID)
}
delete self.pendingMessages[messageID];
}
}
if(hasApiCall && !hasHttpWait && this.transport instanceof HTTP) {
var serializer = new TLSerialization({mtproto: true});
serializer.storeMethod('http_wait', {
max_delay: 500,
wait_after: 150,
max_wait: 3000
});
messages.push({
msg_id: timeManager.generateID(),
seq_no: this.generateSeqNo(),
body: serializer.getBytes()
});
}
if(!messages.length) {
// this.log('no scheduled messages')
return;
}
var noResponseMsgs: Array<string> = [];
if(messages.length > 1) {
var container = new TLSerialization({
mtproto: true,
startMaxLength: messagesByteLen + 64
});
container.storeInt(0x73f1f8dc, 'CONTAINER[id]');
container.storeInt(messages.length, 'CONTAINER[count]');
var innerMessages: string[] = [];
messages.forEach((message, i) => {
container.storeLong(message.msg_id, 'CONTAINER[' + i + '][msg_id]');
innerMessages.push(message.msg_id);
container.storeInt(message.seq_no, 'CONTAINER[' + i + '][seq_no]');
container.storeInt(message.body.length, 'CONTAINER[' + i + '][bytes]');
container.storeRawBytes(message.body, 'CONTAINER[' + i + '][body]');
if(message.noResponse) {
noResponseMsgs.push(message.msg_id);
}
});
var containerSentMessage: Message = {
msg_id: timeManager.generateID(),
seq_no: this.generateSeqNo(true),
container: true,
inner: innerMessages
};
message = Object.assign({
body: container.getBytes(true)
}, containerSentMessage);
this.sentMessages[message.msg_id] = containerSentMessage;
if(Modes.debug || true) {
this.log('Container', innerMessages, message.msg_id, message.seq_no);
}
} else {
if(message.noResponse) {
noResponseMsgs.push(message.msg_id);
}
this.sentMessages[message.msg_id] = message;
}
this.pendingAcks = [];
let promise = this.sendEncryptedRequest(message);
if(!(this.transport instanceof HTTP)) {
if(noResponseMsgs.length) this.log.error('noResponseMsgs length!', noResponseMsgs);
} else promise.then((result) => {
self.toggleOffline(false);
// this.log('parse for', message)
self.parseResponse(result).then((response) => {
if(Modes.debug) {
this.log('Server response', self.dcID, response);
}
self.processMessage(response.response, response.messageID, response.sessionID);
noResponseMsgs.forEach((msgID) => {
if(self.sentMessages[msgID]) {
var deferred = self.sentMessages[msgID].deferred;
delete self.sentMessages[msgID];
deferred.resolve();
}
});
if(self.transport instanceof HTTP) {
self.checkLongPoll();
this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod));
}
});
}, (error) => {
this.log.error('Encrypted request failed', error, message);
if(message.container) {
message.inner.forEach((msgID: string) => {
self.pendingMessages[msgID] = 0;
});
delete self.sentMessages[message.msg_id];
} else {
self.pendingMessages[message.msg_id] = 0;
}
noResponseMsgs.forEach((msgID) => {
if(self.sentMessages[msgID]) {
var deferred = self.sentMessages[msgID].deferred;
delete self.sentMessages[msgID];
delete self.pendingMessages[msgID];
deferred.reject();
}
})
self.toggleOffline(true);
});
if(lengthOverflow || singlesCount > 1) {
this.scheduleRequest();
}
}
public async getEncryptedMessage(dataWithPadding: ArrayBuffer) {
let msgKey = await this.getMsgKey(dataWithPadding, true);
let keyIv = await this.getAesKeyIv(msgKey, true);
// this.log('after msg key iv')
let encryptedBytes = await CryptoWorker.aesEncrypt(dataWithPadding, keyIv[0], keyIv[1]);
// this.log('Finish encrypt')
return {
bytes: encryptedBytes,
msgKey: msgKey
};
}
public getDecryptedMessage(msgKey: Uint8Array | number[], encryptedData: Uint8Array | number[]): Promise<ArrayBuffer> {
// this.log('get decrypted start')
return this.getAesKeyIv(msgKey, false).then((keyIv) => {
// this.log('after msg key iv')
return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]);
});
}
public sendEncryptedRequest(message: any, options: any = {}) {
var self = this;
this.log.debug('Send encrypted', message, options, this.authKeyID);
// console.trace()
var data = new TLSerialization({
startMaxLength: message.body.length + 2048
});
data.storeIntBytes(this.serverSalt, 64, 'salt');
data.storeIntBytes(this.sessionID, 64, 'session_id');
data.storeLong(message.msg_id, 'message_id');
data.storeInt(message.seq_no, 'seq_no');
data.storeInt(message.body.length, 'message_data_length');
data.storeRawBytes(message.body, 'message_data');
var dataBuffer = data.getBuffer();
var paddingLength = (16 - (data.offset % 16)) + 16 * (1 + nextRandomInt(5));
var padding = new Array(paddingLength);
padding = [...new Uint8Array(padding.length).randomize()];
//MTProto.secureRandom.nextBytes(padding);
var dataWithPadding = bufferConcat(dataBuffer, padding);
// this.log('Adding padding', dataBuffer, padding, dataWithPadding)
// this.log('auth_key_id', bytesToHex(self.authKeyID))
return this.getEncryptedMessage(dataWithPadding).then((encryptedResult) => {
this.log.debug('Got encrypted out message', encryptedResult);
let request = new TLSerialization({
startMaxLength: encryptedResult.bytes.byteLength + 256
});
request.storeIntBytes(self.authKeyID, 64, 'auth_key_id');
request.storeIntBytes(encryptedResult.msgKey, 128, 'msg_key');
request.storeRawBytes(encryptedResult.bytes, 'encrypted_data');
//var requestData = xhrSendBuffer ? request.getBuffer() : request.getBytes(true) as Uint8Array;
let requestData = request.getBytes(true);
let baseError = {
code: 406,
type: 'NETWORK_BAD_RESPONSE',
transport: this.transport
};
let promise = this.transport.send(requestData);
if(!(this.transport instanceof HTTP)) return promise;
return promise.then((result) => {
if(!result || !result.byteLength) {
return Promise.reject(baseError);
}
return result;
}, (error) => {
if(!error.message && !error.type) {
error = Object.assign(baseError, {
type: 'NETWORK_BAD_REQUEST',
originalError: error
});
}
return Promise.reject(error);
});
});
}
public parseResponse(responseBuffer: Uint8Array) {
this.log.debug('Start parsing response'/* , responseBuffer */);
let self = this;
let deserializer = new TLDeserialization(responseBuffer);
let authKeyID = deserializer.fetchIntBytes(64, true, 'auth_key_id');
if(!bytesCmp(authKeyID, this.authKeyID)) {
throw new Error('[MT] Invalid server auth_key_id: ' + authKeyID.hex);
}
let msgKey = deserializer.fetchIntBytes(128, true, 'msg_key');
let encryptedData = deserializer.fetchRawBytes(responseBuffer.byteLength - deserializer.getOffset(), true, 'encrypted_data');
return self.getDecryptedMessage(msgKey, encryptedData).then((dataWithPadding) => {
// this.log('after decrypt')
return self.getMsgKey(dataWithPadding, false).then((calcMsgKey) => {
if(!bytesCmp(msgKey, calcMsgKey)) {
this.log.warn('[MT] msg_keys', msgKey, calcMsgKey);
this.updateSession(); // fix 28.01.2020
throw new Error('[MT] server msgKey mismatch, updating session');
}
// this.log('after msgKey check')
let deserializer = new TLDeserialization(dataWithPadding, {mtproto: true});
/* let salt = */deserializer.fetchIntBytes(64, false, 'salt'); // need
let sessionID = deserializer.fetchIntBytes(64, false, 'session_id');
let messageID = deserializer.fetchLong('message_id');
if(!bytesCmp(sessionID, self.sessionID) &&
(!self.prevSessionID || !bytesCmp(sessionID, self.prevSessionID))) {
this.log.warn('Sessions', sessionID, self.sessionID, self.prevSessionID, dataWithPadding);
//this.updateSession();
//this.sessionID = sessionID;
throw new Error('[MT] Invalid server session_id: ' + bytesToHex(sessionID));
}
let seqNo = deserializer.fetchInt('seq_no');
let totalLength = dataWithPadding.byteLength;
let messageBodyLength = deserializer.fetchInt('message_data[length]');
let offset = deserializer.getOffset();
if((messageBodyLength % 4) ||
messageBodyLength > totalLength - offset) {
throw new Error('[MT] Invalid body length: ' + messageBodyLength);
}
let messageBody = deserializer.fetchRawBytes(messageBodyLength, true, 'message_data');
offset = deserializer.getOffset();
let paddingLength = totalLength - offset;
if(paddingLength < 12 || paddingLength > 1024) {
throw new Error('[MT] Invalid padding length: ' + paddingLength);
}
//let buffer = bytesToArrayBuffer(messageBody);
deserializer = new TLDeserialization(/* buffer */messageBody, {mtproto: true});
// костыль
deserializer.override = {
mt_message: (function(this: TLDeserialization, result: any, field: string) {
result.msg_id = this.fetchLong(field + '[msg_id]');
result.seqno = this.fetchInt(field + '[seqno]');
result.bytes = this.fetchInt(field + '[bytes]');
var offset = this.getOffset();
//self.log('mt_message!!!!!', result, field);
try {
result.body = this.fetchObject('Object', field + '[body]');
} catch(e) {
self.log.error('parse error', e.message, e.stack);
result.body = {
_: 'parse_error',
error: e
};
}
if(this.offset != offset + result.bytes) {
// console.warn(dT(), 'set offset', this.offset, offset, result.bytes)
// this.log(result)
this.offset = offset + result.bytes;
}
// this.log('override message', result)
}).bind(deserializer),
mt_rpc_result: (function(this: TLDeserialization, result: any, field: any) {
result.req_msg_id = this.fetchLong(field + '[req_msg_id]');
var sentMessage = self.sentMessages[result.req_msg_id];
var type = sentMessage && sentMessage.resultType || 'Object';
if(result.req_msg_id && !sentMessage) {
// console.warn(dT(), 'Result for unknown message', result);
return;
}
result.result = this.fetchObject(type, field + '[result]');
// self.log(dT(), 'override rpc_result', sentMessage, type, result);
}).bind(deserializer)
};
var response = deserializer.fetchObject('', 'INPUT');
return {
response: response,
messageID: messageID,
sessionID: sessionID,
seqNo: seqNo
};
});
});
}
public applyServerSalt(newServerSalt: string) {
var serverSalt = longToBytes(newServerSalt);
AppStorage.set({
['dc' + this.dcID + '_server_salt']: bytesToHex(serverSalt)
});
this.serverSalt = serverSalt;
return true;
}
public scheduleRequest(delay = 0) {
if(!(this.transport instanceof HTTP)) { // if socket
return this.performScheduledRequest();
}
if(this.offline/* && this.transport instanceof HTTP */) {
this.checkConnection('forced schedule');
}
/* if(delay && !(this.transport instanceof HTTP)) {
delay = 0;
} */
var nextReq = Date.now() + delay;
if(delay && this.nextReq && this.nextReq <= nextReq) {
return false;
}
// this.log('schedule req', delay)
// console.trace()
clearTimeout(this.nextReqTimeout);
this.nextReqTimeout = 0;
if(delay > 0) {
this.nextReqTimeout = setTimeout(this.performScheduledRequest.bind(this), delay || 0);
} else {
setTimeout(this.performScheduledRequest.bind(this), 0);
}
this.nextReq = nextReq;
}
public ackMessage(msgID: string) {
// this.log('ack message', msgID)
this.pendingAcks.push(msgID);
this.scheduleRequest(30000);
}
public reqResendMessage(msgID: string) {
this.log('Req resend', msgID);
this.pendingResends.push(msgID);
this.scheduleRequest(100);
}
public cleanupSent() {
let notEmpty = false;
// this.log('clean start', this.dcID/*, this.sentMessages*/)
Object.keys(this.sentMessages).forEach((msgID) => {
const message = this.sentMessages[msgID];
// this.log('clean iter', msgID, message)
if(message.notContentRelated && this.pendingMessages[msgID] === undefined) {
// this.log('clean notContentRelated', msgID)
delete this.sentMessages[msgID];
} else if(message.container) {
for(let i = 0; i < message.inner.length; i++) {
if(this.sentMessages[message.inner[i]] !== undefined) {
// this.log('clean failed, found', msgID, message.inner[i], this.sentMessages[message.inner[i]].seq_no)
notEmpty = true;
return;
}
}
// this.log('clean container', msgID)
delete this.sentMessages[msgID];
} else {
notEmpty = true;
}
});
return !notEmpty;
}
public processMessageAck(messageID: string) {
var sentMessage = this.sentMessages[messageID];
if(sentMessage && !sentMessage.acked) {
delete sentMessage.body;
sentMessage.acked = true;
return true;
}
return false;
}
public processError(rawError: {error_message: string, error_code: number}) {
var matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || [];
rawError.error_code = rawError.error_code;
return {
code: !rawError.error_code || rawError.error_code <= 0 ? 500 : rawError.error_code,
type: matches[1] || 'UNKNOWN',
description: matches[3] || ('CODE#' + rawError.error_code + ' ' + rawError.error_message),
originalError: rawError
};
}
/**
* только для сокета, возможно это будет неправильно работать, но в тесте сработало правильно
*/
public resend() {
for(let id in this.sentMessages) {
const msg = this.sentMessages[id];
if(msg.body) {
this.pushResend(id);
}
}
}
public processMessage(message: any, messageID: string, sessionID: Uint8Array | number[]) {
var msgidInt = parseInt(messageID/* .toString(10) */.substr(0, -10), 10);
if(msgidInt % 2) {
this.log.warn('[MT] Server even message id: ', messageID, message);
return;
}
this.log.debug('process message', message, messageID, sessionID);
switch(message._) {
case 'msg_container':
var len = message.messages.length;
for(var i = 0; i < len; i++) {
this.processMessage(message.messages[i], message.messages[i].msg_id, sessionID);
}
break;
case 'bad_server_salt':
this.log('Bad server salt', message);
var sentMessage = this.sentMessages[message.bad_msg_id];
if(!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
this.log(message.bad_msg_id, message.bad_msg_seqno);
throw new Error('[MT] Bad server salt for invalid message');
}
this.applyServerSalt(message.new_server_salt);
this.pushResend(message.bad_msg_id);
this.ackMessage(messageID);
/* // simulate disconnect
try {
this.log('networker state:', this);
// @ts-ignore
this.transport.ws.close(1000);
} catch(err) {
this.log.error('transport', this.transport, err);
} */
break;
case 'bad_msg_notification':
this.log('Bad msg notification', message);
var sentMessage = this.sentMessages[message.bad_msg_id];
if(!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
this.log(message.bad_msg_id, message.bad_msg_seqno);
throw new Error('[MT] Bad msg notification for invalid message');
}
if(message.error_code == 16 || message.error_code == 17) {
if(timeManager.applyServerTime(
bigStringInt(messageID).shiftRight(32).toString(10)
)) {
this.log('Update session');
this.updateSession();
}
var badMessage = this.updateSentMessage(message.bad_msg_id);
if(badMessage) this.pushResend(badMessage.msg_id); // fix 23.01.2020
this.ackMessage(messageID);
}
break;
case 'message':
if(this.lastServerMessages.indexOf(messageID) != -1) {
// console.warn('[MT] Server same messageID: ', messageID)
this.ackMessage(messageID);
return;
}
this.lastServerMessages.push(messageID);
if(this.lastServerMessages.length > 100) {
this.lastServerMessages.shift();
}
this.processMessage(message.body, message.msg_id, sessionID);
break;
case 'new_session_created':
this.ackMessage(messageID);
this.log.debug('new_session_created', message);
//this.updateSession();
this.processMessageAck(message.first_msg_id);
this.applyServerSalt(message.server_salt);
AppStorage.get<number>('dc').then((baseDcID: number) => {
if(baseDcID == this.dcID && !this.upload && NetworkerFactory.updatesProcessor) {
NetworkerFactory.updatesProcessor(message, true);
}
});
break;
case 'msgs_ack':
for(var i = 0; i < message.msg_ids.length; i++) {
this.processMessageAck(message.msg_ids[i]);
}
break;
case 'msg_detailed_info':
if(!this.sentMessages[message.msg_id]) {
this.ackMessage(message.answer_msg_id);
break;
}
case 'msg_new_detailed_info':
if(this.pendingAcks.indexOf(message.answer_msg_id)) {
break;
}
this.reqResendMessage(message.answer_msg_id);
break;
case 'msgs_state_info':
this.ackMessage(message.answer_msg_id);
if(this.lastResendReq &&
this.lastResendReq.req_msg_id == message.req_msg_id &&
this.pendingResends.length
) {
var badMsgID, pos;
for(let i = 0; i < this.lastResendReq.resend_msg_ids.length; i++) {
badMsgID = this.lastResendReq.resend_msg_ids[i];
pos = this.pendingResends.indexOf(badMsgID);
if(pos != -1) {
this.pendingResends.splice(pos, 1);
}
}
}
break;
case 'rpc_result':
this.ackMessage(messageID);
var sentMessageID = message.req_msg_id;
var sentMessage = this.sentMessages[sentMessageID];
this.processMessageAck(sentMessageID);
if(sentMessage) {
var deferred = sentMessage.deferred;
if(message.result._ == 'rpc_error') {
var error = this.processError(message.result);
this.log('Rpc error', error);
if(deferred) {
deferred.reject(error);
}
} else {
if(deferred) {
if(Modes.debug) {
this.log.debug('Rpc response', message.result);
} else {
var dRes = message.result._;
if(!dRes) {
if(message.result.length > 5) {
dRes = '[..' + message.result.length + '..]';
} else {
dRes = message.result;
}
}
this.log.debug('Rpc response', dRes, sentMessage);
}
sentMessage.deferred.resolve(message.result);
}
if(sentMessage.isAPI && !this.connectionInited) {
this.connectionInited = true;
////this.log('Rpc set connectionInited to:', this.connectionInited);
}
}
delete this.sentMessages[sentMessageID];
}
break;
default:
this.ackMessage(messageID);
this.log.debug('Update', message);
if(NetworkerFactory.updatesProcessor !== null) {
NetworkerFactory.updatesProcessor(message, true);
}
break;
}
}
}
export {MTPNetworker};