import {isObject} from './bin_utils'; import { bigStringInt} from './bin_utils'; import {TLDeserialization, TLSerialization} from './tl_utils'; import CryptoWorker from '../crypto/cryptoworker'; import sessionStorage from '../sessionStorage'; import Schema from './schema'; import timeManager from './timeManager'; import NetworkerFactory from './networkerFactory'; import { logger, LogLevels } from '../logger'; import { Modes, App, DEBUG } from './mtproto_config'; import { InvokeApiOptions } from '../../types'; import { longToBytes } from '../crypto/crypto_utils'; import MTTransport from './transports/transport'; import { convertToUint8Array, bufferConcat, bytesCmp, bytesToHex } from '../../helpers/bytes'; import { nextRandomInt, randomLong } from '../../helpers/random'; import { CancellablePromise, deferredPromise } from '../../helpers/cancellablePromise'; import { isSafari } from '../../helpers/userAgent'; /// #if MTPROTO_HTTP_UPLOAD // @ts-ignore import HTTP from './transports/http'; // @ts-ignore import Socket from './transports/websocket'; /// #elif MTPROTO_HTTP // @ts-ignore import HTTP from './transports/http'; /// #else // @ts-ignore import Socket from './transports/websocket'; /// #endif //console.error('networker included!', new Error().stack); export type MTMessageOptions = InvokeApiOptions & Partial<{ noResponse: true, // http_wait longPoll: true, notContentRelated: true, // ACK noSchedule: true, messageId: string, }>; export type MTMessage = InvokeApiOptions & MTMessageOptions & { msg_id: string, seq_no: number, body?: Uint8Array | number[], isAPI?: boolean, // only these four are important acked?: boolean, deferred?: { resolve: any, reject: any }, container?: boolean, inner?: string[], // below - options notContentRelated?: true, noSchedule?: true, resultType?: string, longPoll?: true, noResponse?: true, // only with http (http_wait for longPoll) }; const CONNECTION_TIMEOUT = 5000; export default class MTPNetworker { private authKeyUint8: Uint8Array; private isFileNetworker: boolean; private isFileUpload: boolean; private isFileDownload: boolean; private lastServerMessages: Array = []; private sentMessages: { [msgId: string]: MTMessage } = {}; private pendingMessages: {[msgId: string]: number} = {}; private pendingAcks: Array = []; private pendingResends: Array = []; private connectionInited = false; /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD //private longPollInt: number; private longPollPending = 0; private nextReqTimeout: number; private nextReq: number = 0; private checkConnectionTimeout: number; private checkConnectionPeriod = 0; private sleepAfter = 0; private offline = false; /// #endif private seqNo: number = 0; private prevSessionId: Uint8Array; private sessionId: Uint8Array; private serverSalt: Uint8Array; private lastResendReq: { req_msg_id: string, resend_msg_ids: Array } | null = null; private name: string; private log: ReturnType; public isOnline = false; private lastResponseTime = 0; private disconnectDelay: number; private pingPromise: CancellablePromise; private sentPingTimes = 0; private tt = 0; //public onConnectionStatusChange: (online: boolean) => void; private debugRequests: Array<{before: Uint8Array, after: Uint8Array}> = []; constructor(public dcId: number, private authKey: number[], private authKeyId: Uint8Array, serverSalt: number[], private transport: MTTransport, options: InvokeApiOptions = {}) { this.authKeyUint8 = convertToUint8Array(this.authKey); this.serverSalt = convertToUint8Array(serverSalt); this.isFileUpload = !!options.fileUpload; this.isFileDownload = !!options.fileDownload; this.isFileNetworker = this.isFileUpload || this.isFileDownload; const suffix = this.isFileUpload ? '-U' : this.isFileDownload ? '-D' : ''; this.name = 'NET-' + dcId + suffix; //this.log = logger(this.name, this.upload && this.dcId === 2 ? LogLevels.debug | LogLevels.warn | LogLevels.log | LogLevels.error : LogLevels.error); this.log = logger(this.name, LogLevels.log | LogLevels.error | LogLevels.debug | LogLevels.warn); this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */); // Test resend after bad_server_salt /* if(this.dcId === 2 && this.upload) { //timeManager.applyServerTime((Date.now() / 1000 - 86400) | 0); this.serverSalt[0] = 0; } */ this.updateSession(); // if(!NetworkerFactory.offlineInited) { // NetworkerFactory.offlineInited = true; // /* rootScope.offline = true // rootScope.offlineConnecting = true */ // } /// #if MTPROTO_HTTP_UPLOAD if(this.transport instanceof HTTP) { /* this.longPollInt = */setInterval(this.checkLongPoll, 10000); this.checkLongPoll(); } else { (this.transport as Socket).networker = this; } /// #elif MTPROTO_HTTP //if(this.transport instanceof HTTP) { /* this.longPollInt = */setInterval(this.checkLongPoll, 10000); this.checkLongPoll(); /// #else //} else { (this.transport as Socket).networker = this; //} /// #endif // * handle outcoming dead socket, server will close the connection if((this.transport as Socket).networker) { if(isSafari) { this.pingPromise = Promise.resolve(); } else { this.disconnectDelay = (this.transport as Socket).retryTimeout / 1000 | 0; //setInterval(this.sendPingDelayDisconnect, (this.disconnectDelay - 5) * 1000); this.sendPingDelayDisconnect(); } } } public updateSession() { this.seqNo = 0; this.prevSessionId = this.sessionId; this.sessionId = new Uint8Array(8).randomize(); } public updateSentMessage(sentMessageId: string) { const sentMessage = this.sentMessages[sentMessageId]; if(!sentMessage) { return false; } if(sentMessage.container) { sentMessage.inner.forEachReverse((innerSentMessageId, idx) => { const innerSentMessage = this.updateSentMessage(innerSentMessageId); if(!innerSentMessage) { sentMessage.inner.splice(idx, 1); } }); } sentMessage.msg_id = timeManager.generateId(); sentMessage.seq_no = this.generateSeqNo(sentMessage.notContentRelated || sentMessage.container); /* if(DEBUG) { this.log('updateSentMessage', sentMessage.msg_id, sentMessageId); } */ this.sentMessages[sentMessage.msg_id] = sentMessage; delete this.sentMessages[sentMessageId]; return sentMessage; } public generateSeqNo(notContentRelated?: boolean) { let seqNo = this.seqNo * 2; if(!notContentRelated) { seqNo++; this.seqNo++; } return seqNo; } public wrapMtpCall(method: string, params: any, options: MTMessageOptions) { const serializer = new TLSerialization({mtproto: true}); serializer.storeMethod(method, params); const messageId = timeManager.generateId(); const seqNo = this.generateSeqNo(); const message = { msg_id: messageId, seq_no: seqNo, body: serializer.getBytes(true) }; if(Modes.debug) { this.log('MT call', method, params, messageId, seqNo); } return this.pushMessage(message, options); } public wrapMtpMessage(object: any, options: MTMessageOptions) { const serializer = new TLSerialization({mtproto: true}); serializer.storeObject(object, 'Object'); const messageId = timeManager.generateId(); const seqNo = this.generateSeqNo(options.notContentRelated); const message = { msg_id: messageId, seq_no: seqNo, body: serializer.getBytes(true) }; if(Modes.debug) { this.log('MT message', object, messageId, seqNo); } return this.pushMessage(message, options); } public wrapApiCall(method: string, params: any = {}, options: InvokeApiOptions = {}) { const serializer = new TLSerialization(options); if(!this.connectionInited) { // this will call once for each new session ///////this.log('Wrap api call !this.connectionInited'); const 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'); const 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) { const invokeAfterMsg = Schema.API.methods.find(m => m.method === 'invokeAfterMsg'); if(!invokeAfterMsg) throw new Error('no invokeAfterMsg!'); if(DEBUG) { 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)); } */ const messageId = timeManager.generateId(); const seqNo = this.generateSeqNo(); const 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 if(DEBUG) { this.log('Api call', method, params, options); } return this.pushMessage(message, options); } private sendPingDelayDisconnect = () => { if(this.pingPromise || true) return; if(!this.isOnline) { if((this.transport as Socket).connected) { (this.transport as Socket).handleClose(); } return; } this.log('sendPingDelayDisconnect', this.sentPingTimes); /* if(this.tt) clearTimeout(this.tt); this.tt = self.setTimeout(() => { (this.transport as any).ws.close(1000); this.tt = 0; }, this.disconnectDelay * 1000); */ /* this.wrapMtpCall('ping_delay_disconnect', { ping_id: randomLong(), disconnect_delay: this.disconnectDelay }, { noResponse: true, notContentRelated: true }); */ const deferred = this.pingPromise = deferredPromise(); const timeoutTime = this.disconnectDelay * 1000; /* if(!this.sentPingTimes || true) { ++this.sentPingTimes; */ const startTime = Date.now(); this.wrapMtpCall('ping', { ping_id: randomLong() }, {}).then(pong => { const elapsedTime = Date.now() - startTime; this.log('sendPingDelayDisconnect: response', pong, elapsedTime > timeoutTime); if(elapsedTime > timeoutTime) { deferred.reject(); } else { setTimeout(deferred.resolve, timeoutTime - elapsedTime); } }, deferred.reject).finally(() => { clearTimeout(rejectTimeout); //--this.sentPingTimes; }); //} const rejectTimeout = self.setTimeout(deferred.reject, timeoutTime); deferred.catch(() => { (this.transport as Socket).handleClose(); }); deferred.finally(() => { this.pingPromise = null; this.sendPingDelayDisconnect(); }); }; /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD public checkLongPoll = () => { const isClean = this.cleanupSent(); //this.log.error('Check lp', this.longPollPending, this.dcId, isClean, this); if((this.longPollPending && Date.now() < this.longPollPending) || this.offline) { //this.log('No lp this time'); return false; } sessionStorage.get('dc').then((baseDcId) => { if(isClean && ( baseDcId !== this.dcId || this.isFileNetworker || (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() { const 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, 0); }, (error: ErrorEvent) => { this.log('Long-poll failed', error); }); } public checkConnection = (event: Event | string) => { /* rootScope.offlineConnecting = true */ this.log('Check connection', event); clearTimeout(this.checkConnectionTimeout); this.checkConnectionTimeout = 0; const serializer = new TLSerialization({mtproto: true}); const pingId = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)]; serializer.storeMethod('ping', { ping_id: pingId }); const pingMessage = { msg_id: timeManager.generateId(), seq_no: this.generateSeqNo(true), body: serializer.getBytes() }; this.sendEncryptedRequest(pingMessage).then((result) => { /* delete rootScope.offlineConnecting */ this.toggleOffline(false); }, () => { this.log('Delay ', this.checkConnectionPeriod * 1000); this.checkConnectionTimeout = setTimeout(this.checkConnection, this.checkConnectionPeriod * 1000 | 0); this.checkConnectionPeriod = Math.min(60, this.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.offline) { clearTimeout(this.nextReqTimeout); this.nextReqTimeout = 0; this.nextReq = 0; if(this.checkConnectionPeriod < 1.5) { this.checkConnectionPeriod = 0; } this.checkConnectionTimeout = setTimeout(this.checkConnection, this.checkConnectionPeriod * 1000 | 0); this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5); /// #if !MTPROTO_WORKER document.body.addEventListener('online', this.checkConnection, false); document.body.addEventListener('focus', this.checkConnection, false); /// #endif } else { this.checkLongPoll(); this.scheduleRequest(); /// #if !MTPROTO_WORKER document.body.removeEventListener('online', this.checkConnection); document.body.removeEventListener('focus', this.checkConnection); /// #endif clearTimeout(this.checkConnectionTimeout); this.checkConnectionTimeout = 0; } } private handleSentEncryptedRequestHTTP(promise: ReturnType, message: MTMessage, noResponseMsgs: string[]) { promise .then((result) => { this.toggleOffline(false); // this.log('parse for', message) this.parseResponse(result).then((response) => { if(Modes.debug) { this.log('Server response', response); } this.processMessage(response.response, response.messageId, response.sessionId); noResponseMsgs.forEach((msgId) => { if(this.sentMessages[msgId]) { const deferred = this.sentMessages[msgId].deferred; delete this.sentMessages[msgId]; deferred.resolve(); } }); this.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) => { this.pendingMessages[msgId] = 0; }); delete this.sentMessages[message.msg_id]; } else { this.pendingMessages[message.msg_id] = 0; } noResponseMsgs.forEach((msgId) => { if(this.sentMessages[msgId]) { const deferred = this.sentMessages[msgId].deferred; delete this.sentMessages[msgId]; delete this.pendingMessages[msgId]; deferred.reject(); } }) this.toggleOffline(true); }); } /// #endif // тут можно сделать таймаут и выводить дисконнект public pushMessage(message: { msg_id: string, seq_no: number, body: Uint8Array | number[], isAPI?: boolean }, options: MTMessageOptions) { const promise = new Promise((resolve, reject) => { this.sentMessages[message.msg_id] = Object.assign(message, options, options.notContentRelated ? undefined : { deferred: {resolve, reject} } ); //this.log.error('Networker pushMessage:', this.sentMessages[message.msg_id]); this.pendingMessages[message.msg_id] = 0; if(!options.noSchedule) { this.scheduleRequest(); } if(isObject(options)) { options.messageId = message.msg_id; } }); if(!options.notContentRelated && !options.noResponse) { const timeout = setTimeout(() => { if(this.lastResponseTime && (Date.now() - this.lastResponseTime) < CONNECTION_TIMEOUT) { return; } this.log.error('timeout', message); this.setConnectionStatus(false); this.getEncryptedOutput(message).then(bytes => { this.log.error('timeout encrypted', bytes); }); }, CONNECTION_TIMEOUT); promise.finally(() => { clearTimeout(timeout); this.setConnectionStatus(true); }); } return promise; } public setConnectionStatus(online: boolean) { const willChange = this.isOnline !== online; this.isOnline = online; if(willChange) { if(NetworkerFactory.onConnectionStatusChange) { NetworkerFactory.onConnectionStatusChange({ _: 'networkerStatus', online: this.isOnline, dcId: this.dcId, name: this.name, isFileNetworker: this.isFileNetworker, isFileDownload: this.isFileDownload, isFileUpload: this.isFileUpload }); } if(!this.pingPromise && (this.transport as Socket).networker) { this.sendPingDelayDisconnect(); } /* this.sentPingTimes = 0; this.sendPingDelayDisconnect(); */ } /* if(this.onConnectionStatusChange) { this.onConnectionStatusChange(this.isOnline); } */ } public pushResend(messageId: string, delay = 0) { const value = delay ? Date.now() + delay : 0; const sentMessage = this.sentMessages[messageId]; if(sentMessage.container) { for(const innerMsgId of sentMessage.inner) { this.pendingMessages[innerMsgId] = value; } } else { this.pendingMessages[messageId] = value; } if(sentMessage.acked) { this.log.error('pushResend: acked message?', sentMessage); } if(DEBUG) { this.log('pushResend:', messageId, sentMessage, this.pendingMessages); } this.scheduleRequest(delay); } // * correct, fully checked public async getMsgKey(dataWithPadding: ArrayBuffer, isOut: boolean) { const x = isOut ? 0 : 8 const msgKeyLargePlain = bufferConcat(this.authKeyUint8.subarray(88 + x, 88 + x + 32), dataWithPadding); const msgKeyLarge = await CryptoWorker.sha256Hash(msgKeyLargePlain); const msgKey = new Uint8Array(msgKeyLarge).subarray(8, 24); return msgKey; }; // * correct, fully checked public getAesKeyIv(msgKey: Uint8Array | number[], isOut: boolean): Promise<[Uint8Array, Uint8Array]> { const x = isOut ? 0 : 8; const sha2aText = new Uint8Array(52); const sha2bText = new Uint8Array(52); const promises: Array> = []; sha2aText.set(msgKey, 0); sha2aText.set(this.authKeyUint8.subarray(x, x + 36), 16); promises.push(CryptoWorker.sha256Hash(sha2aText)); sha2bText.set(this.authKeyUint8.subarray(40 + x, 40 + x + 36), 0); sha2bText.set(msgKey, 36); promises.push(CryptoWorker.sha256Hash(sha2bText)); return Promise.all(promises).then((results) => { const aesKey = new Uint8Array(32); const aesIv = new Uint8Array(32); const sha2a = new Uint8Array(results[0]); const 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]; }); } private performScheduledRequest() { // this.log('scheduled', this.dcId, this.iii) if(this.pendingAcks.length) { const ackMsgIds: Array = this.pendingAcks.slice(); // this.log('acking messages', ackMsgIDs) this.wrapMtpMessage({ _: 'msgs_ack', msg_ids: ackMsgIds }, { notContentRelated: true, noSchedule: true }); } if(this.pendingResends.length) { const resendMsgIds: Array = this.pendingResends.slice(); const resendOpts: MTMessageOptions = { noSchedule: true, notContentRelated: true, messageId: '' // will set in wrapMtpMessage->pushMessage }; //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 }; } let message: MTPNetworker['sentMessages'][keyof MTPNetworker['sentMessages']]; const messages: typeof message[] = []; const currentTime = Date.now(); let messagesByteLen = 0; let hasApiCall = false; let hasHttpWait = false; let lengthOverflow = false; for(const messageId in this.pendingMessages) { const value = this.pendingMessages[messageId]; if(!value || value >= currentTime) { if(message = this.sentMessages[messageId]) { /* if(message.fileUpload) { this.log('performScheduledRequest message:', message, message.body.length, (message.body as Uint8Array).byteLength, (message.body as Uint8Array).buffer.byteLength); } */ const messageByteLength = 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 } messages.push(message); messagesByteLen += messageByteLength; if(message.isAPI) { hasApiCall = true; } else if(message.longPoll) { hasHttpWait = true; } } else { // this.log(message, messageId) } delete this.pendingMessages[messageId]; } } /// #if MTPROTO_HTTP_UPLOAD if(this.transport instanceof HTTP) /// #endif /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD if(hasApiCall && !hasHttpWait) { const 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() }); } /// #endif if(!messages.length) { // this.log('no scheduled messages') return; } const noResponseMsgs: Array = []; if(messages.length > 1) { const container = new TLSerialization({ mtproto: true, startMaxLength: messagesByteLen + 64 }); container.storeInt(0x73f1f8dc, 'CONTAINER[id]'); container.storeInt(messages.length, 'CONTAINER[count]'); const 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); } }); const containerSentMessage: MTMessage = { 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) { 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 = []; const promise = this.sendEncryptedRequest(message); /// #if MTPROTO_HTTP_UPLOAD if(!(this.transport instanceof HTTP)) { //if(noResponseMsgs.length) this.log.error('noResponseMsgs length!', noResponseMsgs); this.cleanupSent(); // ! WARNING } else { this.handleSentEncryptedRequestHTTP(promise, message, noResponseMsgs); } /// #elif !MTPROTO_HTTP //if(!(this.transport instanceof HTTP)) { //if(noResponseMsgs.length) this.log.error('noResponseMsgs length!', noResponseMsgs); this.cleanupSent(); // ! WARNING //} else { /// #else this.handleSentEncryptedRequestHTTP(promise, message, noResponseMsgs); //} /// #endif if(lengthOverflow) { this.scheduleRequest(); } }; public async getEncryptedMessage(dataWithPadding: ArrayBuffer) { const msgKey = await this.getMsgKey(dataWithPadding, true); const keyIv = await this.getAesKeyIv(msgKey, true); // this.log('after msg key iv') const encryptedBytes = await CryptoWorker.aesEncrypt(dataWithPadding, keyIv[0], keyIv[1]); // this.log('Finish encrypt') return { bytes: encryptedBytes, msgKey }; } public getDecryptedMessage(msgKey: Uint8Array | number[], encryptedData: Uint8Array | number[]): Promise { // 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 getEncryptedOutput(message: MTMessage) { /* if(DEBUG) { this.log.debug('Send encrypted', message, this.authKeyId); } */ /* if(!this.isOnline) { this.log('trying to send message when offline:', Object.assign({}, message)); //debugger; } */ const 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'); const des = new TLDeserialization(data.getBuffer().slice(16)); const desSalt = des.fetchLong(); const desSessionId = des.fetchLong(); if(!this.isOnline) { this.log.error('trying to send message when offline', message, new Uint8Array(des.buffer), desSalt, desSessionId); } /* const messageDataLength = message.body.length; let canBeLength = 0; // bytes canBeLength += 8; canBeLength += 8; canBeLength += 8; canBeLength += 4; canBeLength += 4; canBeLength += message.body.length; */ const dataBuffer = data.getBuffer(); /* if(dataBuffer.byteLength !== canBeLength || !bytesCmp(new Uint8Array(dataBuffer.slice(dataBuffer.byteLength - message.body.length)), new Uint8Array(message.body))) { this.log.error('wrong length', dataBuffer, canBeLength, message.msg_id); } */ const paddingLength = (16 - (data.offset % 16)) + 16 * (1 + nextRandomInt(5)); const padding = (message as any).padding || new Uint8Array(paddingLength).randomize().fill(0); /* const padding = [167, 148, 207, 226, 86, 192, 193, 57, 124, 153, 174, 145, 159, 1, 5, 70, 127, 157, 51, 241, 46, 85, 141, 212, 139, 234, 213, 164, 197, 116, 245, 70, 184, 40, 40, 201, 233, 211, 150, 94, 57, 84, 1, 135, 108, 253, 34, 139, 222, 208, 71, 214, 90, 67, 36, 28, 167, 148, 207, 226, 86, 192, 193, 57, 124, 153, 174, 145, 159, 1, 5, 70, 127, 157, 51, 241, 46, 85, 141, 212, 139, 234, 213, 164, 197, 116, 245, 70, 184, 40, 40, 201, 233, 211, 150, 94, 57, 84, 1, 135, 108, 253, 34, 139, 222, 208, 71, 214, 90, 67, 36, 28].slice(0, paddingLength); */ (message as any).padding = padding; const dataWithPadding = bufferConcat(dataBuffer, padding); // this.log('Adding padding', dataBuffer, padding, dataWithPadding) // this.log('auth_key_id', bytesToHex(self.authKeyID)) /* if(dataWithPadding.byteLength % 16) { this.log.error('aaa', dataWithPadding, paddingLength); } if(message.fileUpload) { this.log('Send encrypted: body length:', (message.body as ArrayBuffer).byteLength, paddingLength, dataWithPadding); } */ // * full next block is correct return this.getEncryptedMessage(dataWithPadding).then((encryptedResult) => { /* if(DEBUG) { this.log('Got encrypted out message', encryptedResult); } */ const request = new TLSerialization({ startMaxLength: encryptedResult.bytes.length + 256 }); request.storeIntBytes(this.authKeyId, 64, 'auth_key_id'); request.storeIntBytes(encryptedResult.msgKey, 128, 'msg_key'); request.storeRawBytes(encryptedResult.bytes, 'encrypted_data'); const requestData = request.getBytes(true); if(this.isFileNetworker) { //this.log('Send encrypted: requestData length:', requestData.length, requestData.length % 16, paddingLength % 16, paddingLength, data.offset, encryptedResult.msgKey.length % 16, encryptedResult.bytes.length % 16); //this.log('Send encrypted: messageId:', message.msg_id, requestData.length); //this.log('Send encrypted:', message, new Uint8Array(bufferConcat(des.buffer, padding)), requestData, this.serverSalt.hex, this.sessionId.hex/* new Uint8Array(des.buffer) */); this.debugRequests.push({before: new Uint8Array(bufferConcat(des.buffer, padding)), after: requestData}); } return requestData; }); } public sendEncryptedRequest(message: MTMessage) { return this.getEncryptedOutput(message).then(requestData => { const promise = this.transport.send(requestData); /// #if !MTPROTO_HTTP && !MTPROTO_HTTP_UPLOAD return promise; /// #else if(!(this.transport instanceof HTTP)) return promise; const baseError = { code: 406, type: 'NETWORK_BAD_RESPONSE', transport: this.transport }; 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); }); /// #endif }); } public parseResponse(responseBuffer: Uint8Array) { //const perf = performance.now(); /* if(DEBUG) { this.log.debug('Start parsing response', responseBuffer); } */ this.lastResponseTime = Date.now(); const deserializer = new TLDeserialization(responseBuffer); const authKeyId = deserializer.fetchIntBytes(64, true, 'auth_key_id'); if(!bytesCmp(authKeyId, this.authKeyId)) { throw new Error('[MT] Invalid server auth_key_id: ' + authKeyId.hex); } const msgKey = deserializer.fetchIntBytes(128, true, 'msg_key'); const encryptedData = deserializer.fetchRawBytes(responseBuffer.byteLength - deserializer.getOffset(), true, 'encrypted_data'); return this.getDecryptedMessage(msgKey, encryptedData).then((dataWithPadding) => { // this.log('after decrypt') return this.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}); /* const salt = */deserializer.fetchIntBytes(64, false, 'salt'); // need const sessionId = deserializer.fetchIntBytes(64, false, 'session_id'); const messageId = deserializer.fetchLong('message_id'); if(!bytesCmp(sessionId, this.sessionId) && (!this.prevSessionId || !bytesCmp(sessionId, this.prevSessionId))) { this.log.warn('Sessions', sessionId, this.sessionId, this.prevSessionId, dataWithPadding); //this.updateSession(); //this.sessionID = sessionID; throw new Error('[MT] Invalid server session_id: ' + bytesToHex(sessionId)); } const seqNo = deserializer.fetchInt('seq_no'); const totalLength = dataWithPadding.byteLength; const messageBodyLength = deserializer.fetchInt('message_data[length]'); let offset = deserializer.getOffset(); if((messageBodyLength % 4) || messageBodyLength > totalLength - offset) { throw new Error('[MT] Invalid body length: ' + messageBodyLength); } const messageBody = deserializer.fetchRawBytes(messageBodyLength, true, 'message_data'); offset = deserializer.getOffset(); const 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, override: { mt_message: (result: any, field: string) => { result.msg_id = deserializer.fetchLong(field + '[msg_id]'); result.seqno = deserializer.fetchInt(field + '[seqno]'); result.bytes = deserializer.fetchInt(field + '[bytes]'); const offset = deserializer.getOffset(); //self.log('mt_message!!!!!', result, field); try { result.body = deserializer.fetchObject('Object', field + '[body]'); } catch(e) { this.log.error('parse error', e.message, e.stack); result.body = { _: 'parse_error', error: e }; } if(deserializer.offset !== offset + result.bytes) { // console.warn(dT(), 'set offset', this.offset, offset, result.bytes) // this.log(result) deserializer.offset = offset + result.bytes; } // this.log('override message', result) }, mt_rpc_result: (result: any, field: any) => { result.req_msg_id = deserializer.fetchLong(field + '[req_msg_id]'); const sentMessage = this.sentMessages[result.req_msg_id]; const type = sentMessage && sentMessage.resultType || 'Object'; if(result.req_msg_id && !sentMessage) { // console.warn(dT(), 'Result for unknown message', result); return; } result.result = deserializer.fetchObject(type, field + '[result]'); // self.log(dT(), 'override rpc_result', sentMessage, type, result); } } }); const response = deserializer.fetchObject('', 'INPUT'); //this.log.error('Parse response time:', performance.now() - perf); return { response, messageId, sessionId, seqNo }; }); }); } public applyServerSalt(newServerSalt: string) { const serverSalt = longToBytes(newServerSalt); sessionStorage.set({ ['dc' + this.dcId + '_server_salt']: bytesToHex(serverSalt) }); this.serverSalt = new Uint8Array(serverSalt); } // ! таймаут очень сильно тормозит скорость работы сокета (даже нулевой) public scheduleRequest(delay = 0) { /// #if !MTPROTO_HTTP && !MTPROTO_HTTP_UPLOAD /* clearTimeout(this.nextReqTimeout); this.nextReqTimeout = self.setTimeout(this.performScheduledRequest.bind(this), delay || 0); return; */ return this.performScheduledRequest(); /// #else if(!(this.transport instanceof HTTP)) return this.performScheduledRequest(); if(this.offline/* && this.transport instanceof HTTP */) { this.checkConnection('forced schedule'); } /* if(delay && !(this.transport instanceof HTTP)) { delay = 0; } */ const nextReq = Date.now() + delay; if(delay && this.nextReq && this.nextReq <= nextReq) { //this.log('scheduleRequest: nextReq', this.nextReq, nextReq); return false; } //this.log('scheduleRequest: delay', delay); /* if(this.nextReqTimeout) { return; } */ const perf = performance.now(); clearTimeout(this.nextReqTimeout); this.nextReqTimeout = self.setTimeout(() => { //this.log('scheduleRequest: timeout delay was:', performance.now() - perf); this.nextReqTimeout = 0; /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD || true if(this.offline) { //this.log('Cancel scheduled'); return false; } this.nextReq = 0; /// #endif this.performScheduledRequest(); }, delay); this.nextReq = nextReq; /// #endif } public ackMessage(msgId: string) { // this.log('ack message', msgID) this.pendingAcks.push(msgId); this.scheduleRequest(30000); } public reqResendMessage(msgId: string) { if(DEBUG) { 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(const innerMsgId of message.inner) { if(this.sentMessages[innerMsgId] !== 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) { const sentMessage = this.sentMessages[messageId]; if(sentMessage && !sentMessage.acked) { delete sentMessage.body; sentMessage.acked = true; } } public processError(rawError: {error_message: string, error_code: number}) { const 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(const id in this.sentMessages) { const msg = this.sentMessages[id]; if(msg.body) { this.pushResend(id); } } } /* public requestMessageStatus() { const ids: string[] = []; for(const id in this.sentMessages) { const message = this.sentMessages[id]; if(message.isAPI && message.fileUpload) { ids.push(message.msg_id); } } this.wrapMtpMessage({ _: 'msgs_state_req', msg_ids: ids }, { notContentRelated: true }).then(res => { this.log('status', res); }); } */ // * https://core.telegram.org/mtproto/service_messages_about_messages#notice-of-ignored-error-message public processMessage(message: any, messageId: string, sessionId: Uint8Array | number[]) { if(message._ === 'messageEmpty') { this.log.warn('processMessage: messageEmpty', message, messageId); return; } const msgidInt = parseInt(messageId.substr(0, -10), 10); if(msgidInt % 2) { this.log.warn('Server even message id: ', messageId, message); return; } /* if(DEBUG) { this.log('process message', message, messageId, sessionId); } */ switch(message._) { case 'msg_container': { for(const innerMessage of message.messages) { this.processMessage(innerMessage, innerMessage.msg_id, sessionId); } break; } case 'bad_server_salt': { this.log('Bad server salt', message); this.applyServerSalt(message.new_server_salt); if(this.sentMessages[message.bad_msg_id]) { 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.error('Bad msg notification', message); if(message.error_code === 16 || message.error_code === 17) { const changedOffset = timeManager.applyServerTime(bigStringInt(messageId).shiftRight(32).toString(10)); if(message.error_code === 17 || changedOffset) { this.log('Update session'); this.updateSession(); } const 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); if(DEBUG) { this.log.debug('new_session_created', message); } //this.updateSession(); this.processMessageAck(message.first_msg_id); this.applyServerSalt(message.server_salt); sessionStorage.get('dc').then((baseDcId) => { if(baseDcId === this.dcId && !this.isFileNetworker && NetworkerFactory.updatesProcessor) { NetworkerFactory.updatesProcessor(message); } }); break; } case 'msgs_ack': { for(const msgId of message.msg_ids) { this.processMessageAck(msgId); } 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 ) { for(const badMsgId of this.lastResendReq.resend_msg_ids) { const pos = this.pendingResends.indexOf(badMsgId); if(pos !== -1) { this.pendingResends.splice(pos, 1); } } } break; } case 'rpc_result': { this.ackMessage(messageId); const sentMessageId = message.req_msg_id; const sentMessage = this.sentMessages[sentMessageId]; this.processMessageAck(sentMessageId); if(sentMessage) { const deferred = sentMessage.deferred; if(message.result._ === 'rpc_error') { const error = this.processError(message.result); this.log('Rpc error', error); if(deferred) { deferred.reject(error); } } else { if(deferred) { /* if(DEBUG) { this.log.debug('Rpc response', message.result, 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]; } else { if(DEBUG) { this.log('Rpc result for unknown message:', sentMessageId, message); } } break; } case 'pong': { // * https://core.telegram.org/mtproto/service_messages#ping-messages-pingpong - These messages doesn't require acknowledgments if((this.transport as Socket).networker) { const sentMessageId = message.msg_id; const sentMessage = this.sentMessages[sentMessageId]; if(sentMessage) { sentMessage.deferred.resolve(message); delete this.sentMessages[sentMessageId]; } } break; } default: this.ackMessage(messageId); /* if(DEBUG) { this.log.debug('Update', message); } */ if(NetworkerFactory.updatesProcessor !== null) { NetworkerFactory.updatesProcessor(message); } break; } } }