
Fix chat date blinking Fix displaying sent messages to new dialog Scroll to date bubble if message is bigger than viewport Fix releasing keyboard by inline helper Fix clearing self user Fix displaying sent public poll Update contacts counter in dialogs placeholder Improve multiselect animation Disable lottie icon animations if they're disabled Fix changing mtproto transport during authorization
1766 lines
55 KiB
TypeScript
1766 lines
55 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*
|
|
* Originally from:
|
|
* https://github.com/zhukov/webogram
|
|
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
|
|
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
|
*/
|
|
|
|
import {isObject, sortLongsArray} 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, LogTypes } from '../logger';
|
|
import { InvokeApiOptions } from '../../types';
|
|
import { longToBytes } from '../crypto/crypto_utils';
|
|
import MTTransport from './transports/transport';
|
|
import { convertToUint8Array, bytesCmp, bytesToHex, bufferConcats } from '../../helpers/bytes';
|
|
import { nextRandomUint, randomLong } from '../../helpers/random';
|
|
import App from '../../config/app';
|
|
import DEBUG from '../../config/debug';
|
|
import Modes from '../../config/modes';
|
|
import noop from '../../helpers/noop';
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
import HTTP from './transports/http';
|
|
/// #endif
|
|
|
|
import type TcpObfuscated from './transports/tcpObfuscated';
|
|
import { bigInt2str, rightShift_, str2bigInt } from '../../vendor/leemon';
|
|
import { forEachReverse } from '../../helpers/array';
|
|
import { ConnectionStatus } from './connectionStatus';
|
|
import ctx from '../../environment/ctx';
|
|
import dcConfigurator, { DcConfigurator } from './dcConfigurator';
|
|
|
|
//console.error('networker included!', new Error().stack);
|
|
|
|
export type MTMessageOptions = InvokeApiOptions & Partial<{
|
|
noResponse: true, // http_wait
|
|
longPoll: true,
|
|
|
|
notContentRelated: true, // ACK
|
|
noSchedule: true,
|
|
messageId: MTLong,
|
|
}>;
|
|
|
|
export type MTMessage = InvokeApiOptions & MTMessageOptions & {
|
|
msg_id: MTLong,
|
|
seq_no: number,
|
|
body?: Uint8Array | number[],
|
|
isAPI?: boolean,
|
|
// only these four are important
|
|
|
|
acked?: boolean,
|
|
|
|
deferred?: {
|
|
resolve: any,
|
|
reject: any
|
|
},
|
|
|
|
container?: boolean,
|
|
inner?: MTLong[],
|
|
|
|
// below - options
|
|
|
|
notContentRelated?: true,
|
|
noSchedule?: true,
|
|
|
|
resultType?: string,
|
|
|
|
longPoll?: true,
|
|
noResponse?: true, // only with http (http_wait for longPoll)
|
|
};
|
|
|
|
const CONNECTION_TIMEOUT = 5000;
|
|
const DRAIN_TIMEOUT = 10000;
|
|
let invokeAfterMsgConstructor: number;
|
|
|
|
export default class MTPNetworker {
|
|
private authKeyUint8: Uint8Array;
|
|
|
|
public isFileNetworker: boolean;
|
|
private isFileUpload: boolean;
|
|
private isFileDownload: boolean;
|
|
|
|
private lastServerMessages: Array<MTLong> = [];
|
|
|
|
private sentMessages: {
|
|
[msgId: MTLong]: MTMessage
|
|
} = {};
|
|
|
|
private pendingMessages: {[msgId: MTLong]: number} = {};
|
|
private pendingAcks: Array<MTLong> = [];
|
|
private pendingResends: Array<MTLong> = [];
|
|
public connectionInited: boolean;
|
|
|
|
private nextReqTimeout: number;
|
|
private nextReq: number = 0;
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
private longPollInterval: number;
|
|
private longPollPending: number;
|
|
private checkConnectionRetryAt: number;
|
|
private checkConnectionTimeout: number;
|
|
private checkConnectionPeriod = 0;
|
|
private sleepAfter: number;
|
|
private offline = false;
|
|
private sendingLongPoll: boolean;
|
|
/// #endif
|
|
|
|
private seqNo: number;
|
|
private prevSessionId: Uint8Array;
|
|
private sessionId: Uint8Array;
|
|
private serverSalt: Uint8Array;
|
|
|
|
private lastResendReq: {
|
|
req_msg_id: MTLong,
|
|
resend_msg_ids: Array<MTLong>
|
|
} | null = null;
|
|
|
|
private name: string;
|
|
private log: ReturnType<typeof logger>;
|
|
|
|
public isOnline = false;
|
|
public status: ConnectionStatus = ConnectionStatus.Closed;
|
|
private lastResponseTime = 0;
|
|
|
|
private debug = DEBUG /* && false */ || Modes.debug;
|
|
|
|
public activeRequests = 0;
|
|
|
|
public onDrain: () => void;
|
|
private onDrainTimeout: number;
|
|
|
|
public transport: MTTransport;
|
|
|
|
//private disconnectDelay: number;
|
|
//private pingPromise: CancellablePromise<any>;
|
|
//public onConnectionStatusChange: (online: boolean) => void;
|
|
|
|
//private debugRequests: Array<{before: Uint8Array, after: Uint8Array}> = [];
|
|
|
|
constructor(
|
|
public dcId: number,
|
|
private authKey: Uint8Array,
|
|
private authKeyId: Uint8Array,
|
|
serverSalt: Uint8Array,
|
|
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, LogTypes.Log | LogTypes.Debug | LogTypes.Error | LogTypes.Warn, undefined);
|
|
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 */
|
|
// }
|
|
|
|
// * handle outcoming dead socket, server will close the connection
|
|
// if((this.transport as TcpObfuscated).networker) {
|
|
// this.disconnectDelay = /* (this.transport as TcpObfuscated).retryTimeout */75;
|
|
// //setInterval(this.sendPingDelayDisconnect, (this.disconnectDelay - 5) * 1000);
|
|
// this.sendPingDelayDisconnect();
|
|
// }
|
|
}
|
|
|
|
private updateSession() {
|
|
this.seqNo = 0;
|
|
this.prevSessionId = this.sessionId;
|
|
this.sessionId = new Uint8Array(8).randomize();
|
|
}
|
|
|
|
/* private clearContainers() {
|
|
for(const messageId in this.sentMessages) {
|
|
const message = this.sentMessages[messageId];
|
|
if(message.container) {
|
|
delete this.sentMessages[messageId];
|
|
}
|
|
}
|
|
} */
|
|
|
|
private updateSentMessage(sentMessageId: string) {
|
|
const sentMessage = this.sentMessages[sentMessageId];
|
|
if(!sentMessage) {
|
|
return false;
|
|
}
|
|
|
|
if(sentMessage.container) {
|
|
forEachReverse(sentMessage.inner, (innerSentMessageId, idx) => {
|
|
const innerSentMessage = this.updateSentMessage(innerSentMessageId);
|
|
if(!innerSentMessage) {
|
|
sentMessage.inner.splice(idx, 1);
|
|
} else {
|
|
sentMessage.inner[idx] = innerSentMessage.msg_id;
|
|
}
|
|
});
|
|
}
|
|
|
|
sentMessage.msg_id = timeManager.generateId();
|
|
sentMessage.seq_no = this.generateSeqNo(sentMessage.notContentRelated || sentMessage.container);
|
|
|
|
if(this.debug) {
|
|
this.log(`updateSentMessage, old=${sentMessageId}, new=${sentMessage.msg_id}`);
|
|
}
|
|
|
|
this.sentMessages[sentMessage.msg_id] = sentMessage;
|
|
delete this.sentMessages[sentMessageId];
|
|
|
|
return sentMessage;
|
|
}
|
|
|
|
private 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, '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, 'initConnection');
|
|
serializer.storeInt(0x0, 'flags');
|
|
serializer.storeInt(App.id, 'api_id');
|
|
serializer.storeString(networkerFactory.userAgent || 'Unknown UserAgent', 'device_model');
|
|
serializer.storeString(navigator.platform || 'Unknown Platform', 'system_version');
|
|
serializer.storeString(App.version + (App.isMainDomain ? ' ' + App.suffix : ''), 'app_version');
|
|
serializer.storeString(navigator.language || 'en', 'system_lang_code');
|
|
serializer.storeString(App.langPack, 'lang_pack');
|
|
serializer.storeString(networkerFactory.language, '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) {
|
|
if(invokeAfterMsgConstructor === undefined) {
|
|
const m = Schema.API.methods.find(m => m.method === 'invokeAfterMsg');
|
|
invokeAfterMsgConstructor = m ? +m.id : 0;
|
|
}
|
|
|
|
if(invokeAfterMsgConstructor) {
|
|
// if(this.debug) {
|
|
// this.log('invokeApi: store invokeAfterMsg');
|
|
// }
|
|
|
|
serializer.storeInt(invokeAfterMsgConstructor, 'invokeAfterMsg');
|
|
serializer.storeLong(options.afterMessageId, 'msg_id');
|
|
} else {
|
|
this.log.error('no invokeAfterMsg!');
|
|
}
|
|
}
|
|
|
|
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(this.debug) {
|
|
this.log('Api call', method, params, options);
|
|
}
|
|
|
|
return this.pushMessage(message, options);
|
|
}
|
|
|
|
public changeTransport(transport?: MTTransport) {
|
|
const oldTransport = this.transport;
|
|
if(oldTransport) {
|
|
oldTransport.destroy();
|
|
|
|
DcConfigurator.removeTransport(dcConfigurator.chosenServers, this.transport);
|
|
|
|
if(this.nextReqTimeout) {
|
|
clearTimeout(this.nextReqTimeout);
|
|
this.nextReqTimeout = 0;
|
|
this.nextReq = 0;
|
|
}
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
if(this.longPollInterval !== undefined) {
|
|
clearInterval(this.longPollInterval);
|
|
this.longPollInterval = undefined;
|
|
}
|
|
|
|
this.clearCheckConnectionTimeout();
|
|
/// #endif
|
|
}
|
|
|
|
this.transport = transport;
|
|
if(!transport) {
|
|
return;
|
|
}
|
|
|
|
transport.networker = this;
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
/// #if MTPROTO_HAS_WS
|
|
if(transport instanceof HTTP) {
|
|
/// #endif
|
|
this.longPollInterval = ctx.setInterval(this.checkLongPoll, 10000);
|
|
this.checkLongPoll();
|
|
this.checkConnection('changed transport');
|
|
/// #if MTPROTO_HAS_WS
|
|
}
|
|
/// #endif
|
|
/// #endif
|
|
|
|
if(transport.connected && (transport as TcpObfuscated).connection) {
|
|
this.setConnectionStatus(ConnectionStatus.Connected);
|
|
}
|
|
|
|
this.resend();
|
|
}
|
|
|
|
public destroy() {
|
|
this.changeTransport();
|
|
}
|
|
|
|
public forceReconnectTimeout() {
|
|
if((this.transport as TcpObfuscated).reconnect) {
|
|
(this.transport as TcpObfuscated).reconnect();
|
|
} else {
|
|
this.resend();
|
|
}
|
|
}
|
|
|
|
public forceReconnect() {
|
|
if((this.transport as TcpObfuscated).forceReconnect) {
|
|
(this.transport as TcpObfuscated).forceReconnect();
|
|
} else {
|
|
this.checkConnection('force reconnect');
|
|
}
|
|
}
|
|
|
|
// private sendPingDelayDisconnect = () => {
|
|
// if(this.pingPromise || true) return;
|
|
|
|
// if(!this.isOnline) {
|
|
// if((this.transport as TcpObfuscated).connected) {
|
|
// (this.transport as TcpObfuscated).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<void>();
|
|
|
|
// 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();
|
|
// });
|
|
// };
|
|
|
|
// private sendPingDelayDisconnect = () => {
|
|
// if(this.pingPromise || true) return;
|
|
|
|
// /* if(!this.isOnline) {
|
|
// if((this.transport as TcpObfuscated).connected) {
|
|
// (this.transport as TcpObfuscated).connection.close();
|
|
// }
|
|
|
|
// return;
|
|
// } */
|
|
|
|
// const deferred = this.pingPromise = deferredPromise<void>();
|
|
|
|
// const timeoutTime = this.disconnectDelay * 1000;
|
|
|
|
// const startTime = Date.now();
|
|
// this.wrapMtpCall('ping_delay_disconnect', {
|
|
// ping_id: randomLong(),
|
|
// disconnect_delay: this.disconnectDelay
|
|
// }, {}).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.log.error('sendPingDelayDisconnect: catch, closing connection if exists');
|
|
// (this.transport as TcpObfuscated).connection.close();
|
|
// });
|
|
|
|
// deferred.finally(() => {
|
|
// this.pingPromise = null;
|
|
// this.sendPingDelayDisconnect();
|
|
// });
|
|
// };
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
private 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.isStopped() ||
|
|
this.isFileNetworker) {
|
|
//this.log('No lp this time');
|
|
return false;
|
|
}
|
|
|
|
sessionStorage.get('dc').then((baseDcId) => {
|
|
if(isClean && (
|
|
baseDcId !== this.dcId ||
|
|
(this.sleepAfter && Date.now() > this.sleepAfter)
|
|
)) {
|
|
//console.warn(dT(), 'Send long-poll for DC is delayed', this.dcId, this.sleepAfter);
|
|
return;
|
|
}
|
|
|
|
this.sendLongPoll();
|
|
});
|
|
};
|
|
|
|
private sendLongPoll() {
|
|
if(this.sendingLongPoll) return;
|
|
this.sendingLongPoll = true;
|
|
const maxWait = 25000;
|
|
|
|
this.longPollPending = Date.now() + maxWait;
|
|
this.debug && this.log.debug('sendLongPoll', this.longPollPending);
|
|
|
|
this.wrapMtpCall('http_wait', {
|
|
max_delay: 500,
|
|
wait_after: 150,
|
|
max_wait: maxWait
|
|
}, {
|
|
noResponse: true,
|
|
longPoll: true
|
|
}).then(() => {
|
|
this.longPollPending = undefined;
|
|
setTimeout(this.checkLongPoll, 0);
|
|
}, (error: ErrorEvent) => {
|
|
this.log('Long-poll failed', error);
|
|
}).finally(() => {
|
|
this.sendingLongPoll = undefined;
|
|
});
|
|
}
|
|
|
|
private checkConnection = (event: Event | string) => {
|
|
this.debug && this.log('Check connection', event);
|
|
this.clearCheckConnectionTimeout();
|
|
|
|
if(!this.transport) {
|
|
this.log.warn('No transport for checkConnection');
|
|
return;
|
|
}
|
|
|
|
const serializer = new TLSerialization({mtproto: true});
|
|
const pingId = randomLong();
|
|
|
|
serializer.storeMethod('ping', {
|
|
ping_id: pingId
|
|
});
|
|
|
|
const pingMessage = {
|
|
msg_id: timeManager.generateId(),
|
|
seq_no: this.generateSeqNo(true),
|
|
body: serializer.getBytes(true)
|
|
};
|
|
|
|
if(this.offline) {
|
|
this.setConnectionStatus(ConnectionStatus.Connecting);
|
|
}
|
|
|
|
this.sendEncryptedRequest(pingMessage).then(() => {
|
|
this.toggleOffline(false);
|
|
}, () => {
|
|
this.debug && this.log('Delay', this.checkConnectionPeriod * 1000);
|
|
this.checkConnectionTimeout = ctx.setTimeout(() => this.checkConnection('from failed checkConnection request'), this.checkConnectionPeriod * 1000 | 0);
|
|
this.checkConnectionPeriod = Math.min(60, this.checkConnectionPeriod * 1.5);
|
|
});
|
|
};
|
|
|
|
private clearCheckConnectionTimeout() {
|
|
if(this.checkConnectionTimeout !== undefined) {
|
|
clearTimeout(this.checkConnectionTimeout);
|
|
this.checkConnectionTimeout = undefined;
|
|
}
|
|
}
|
|
|
|
private toggleOffline(offline: boolean) {
|
|
if(this.offline !== offline) {
|
|
this.offline = offline;
|
|
|
|
this.clearCheckConnectionTimeout();
|
|
if(offline) {
|
|
clearTimeout(this.nextReqTimeout);
|
|
this.nextReqTimeout = 0;
|
|
this.nextReq = 0;
|
|
|
|
if(this.checkConnectionPeriod < 1.5) {
|
|
this.checkConnectionPeriod = 0;
|
|
}
|
|
|
|
const delay = this.checkConnectionPeriod * 1000 | 0;
|
|
this.checkConnectionRetryAt = Date.now() + delay;
|
|
this.setConnectionStatus(ConnectionStatus.Closed, this.checkConnectionRetryAt);
|
|
this.checkConnectionTimeout = ctx.setTimeout(() => this.checkConnection('from toggleOfline'), delay);
|
|
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.setConnectionStatus(ConnectionStatus.Connected);
|
|
this.checkLongPoll();
|
|
|
|
this.scheduleRequest();
|
|
|
|
/// #if !MTPROTO_WORKER
|
|
document.body.removeEventListener('online', this.checkConnection);
|
|
document.body.removeEventListener('focus', this.checkConnection);
|
|
/// #endif
|
|
}
|
|
}
|
|
|
|
this.setConnectionStatus(offline ? ConnectionStatus.Closed : ConnectionStatus.Connected, offline ? this.checkConnectionRetryAt : undefined);
|
|
}
|
|
|
|
private handleSentEncryptedRequestHTTP(promise: ReturnType<MTPNetworker['sendEncryptedRequest']>, message: MTMessage, noResponseMsgs: string[]) {
|
|
// let timeout = setTimeout(() => {
|
|
// this.log.error('handleSentEncryptedRequestHTTP timeout', promise, message, noResponseMsgs);
|
|
// }, 5e3);
|
|
|
|
promise.then((result) => {
|
|
this.toggleOffline(false);
|
|
// this.log('parse for', message);
|
|
return this.parseResponse(result).then((response) => {
|
|
this.debug && this.log.debug('Server response', response);
|
|
|
|
this.processMessage(response.response, response.messageId, response.sessionId);
|
|
|
|
this.checkLongPoll();
|
|
this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod));
|
|
|
|
return true;
|
|
});
|
|
}, (error) => {
|
|
this.log.error('Encrypted request failed', error, message);
|
|
|
|
this.pushResend(message.msg_id);
|
|
this.toggleOffline(true);
|
|
|
|
return false;
|
|
}).then((shouldResolve) => {
|
|
// clearTimeout(timeout);
|
|
noResponseMsgs.forEach((msgId) => {
|
|
if(this.sentMessages[msgId]) {
|
|
const deferred = this.sentMessages[msgId].deferred;
|
|
delete this.sentMessages[msgId];
|
|
delete this.pendingMessages[msgId];
|
|
shouldResolve ? deferred.resolve() : deferred.reject();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
/// #endif
|
|
|
|
// тут можно сделать таймаут и выводить дисконнект
|
|
private 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);
|
|
if(this.isOnline) {
|
|
this.setConnectionStatus(ConnectionStatus.TimedOut);
|
|
}
|
|
|
|
/* this.getEncryptedOutput(message).then(bytes => {
|
|
this.log.error('timeout encrypted', bytes);
|
|
}); */
|
|
}, CONNECTION_TIMEOUT);
|
|
|
|
promise.catch(noop).finally(() => {
|
|
clearTimeout(timeout);
|
|
this.setConnectionStatus(ConnectionStatus.Connected);
|
|
|
|
--this.activeRequests;
|
|
this.setDrainTimeout();
|
|
});
|
|
|
|
++this.activeRequests;
|
|
if(this.onDrainTimeout !== undefined) {
|
|
clearTimeout(this.onDrainTimeout);
|
|
this.onDrainTimeout = undefined;
|
|
}
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
public setDrainTimeout() {
|
|
if(!this.activeRequests && this.onDrain && this.onDrainTimeout === undefined) {
|
|
this.onDrainTimeout = ctx.setTimeout(() => {
|
|
this.onDrainTimeout = undefined;
|
|
this.log('drain');
|
|
this.onDrain();
|
|
}, DRAIN_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
public setConnectionStatus(status: ConnectionStatus, retryAt?: number) {
|
|
const isOnline = status === ConnectionStatus.Connected;
|
|
const willChange = this.status !== status;
|
|
this.isOnline = isOnline;
|
|
this.status = status;
|
|
|
|
if(willChange) {
|
|
if(networkerFactory.onConnectionStatusChange) {
|
|
networkerFactory.onConnectionStatusChange({
|
|
_: 'networkerStatus',
|
|
status,
|
|
dcId: this.dcId,
|
|
name: this.name,
|
|
isFileNetworker: this.isFileNetworker,
|
|
isFileDownload: this.isFileDownload,
|
|
isFileUpload: this.isFileUpload,
|
|
retryAt
|
|
});
|
|
}
|
|
|
|
if(this.isOnline) {
|
|
this.scheduleRequest();
|
|
}
|
|
|
|
// if((this.transport as TcpObfuscated).networker) {
|
|
// this.sendPingDelayDisconnect();
|
|
// }
|
|
/* this.sentPingTimes = 0;
|
|
this.sendPingDelayDisconnect(); */
|
|
}
|
|
/* if(this.onConnectionStatusChange) {
|
|
this.onConnectionStatusChange(this.isOnline);
|
|
} */
|
|
}
|
|
|
|
private pushResend(messageId: string, delay = 100) {
|
|
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(this.debug) {
|
|
this.log.debug('pushResend:', messageId, sentMessage, this.pendingMessages, delay);
|
|
}
|
|
|
|
this.scheduleRequest(delay);
|
|
}
|
|
|
|
// * correct, fully checked
|
|
private async getMsgKey(dataWithPadding: Uint8Array, isOut: boolean) {
|
|
const x = isOut ? 0 : 8;
|
|
const msgKeyLargePlain = bufferConcats(this.authKeyUint8.subarray(88 + x, 88 + x + 32), dataWithPadding);
|
|
|
|
const msgKeyLarge = await CryptoWorker.invokeCrypto('sha256-hash', msgKeyLargePlain);
|
|
const msgKey = new Uint8Array(msgKeyLarge).subarray(8, 24);
|
|
return msgKey;
|
|
};
|
|
|
|
// * correct, fully checked
|
|
private getAesKeyIv(msgKey: Uint8Array, isOut: boolean): Promise<[Uint8Array, Uint8Array]> {
|
|
const x = isOut ? 0 : 8;
|
|
const sha2aText = new Uint8Array(52);
|
|
const sha2bText = new Uint8Array(52);
|
|
const promises: Array<Promise<Uint8Array>> = [];
|
|
|
|
sha2aText.set(msgKey, 0);
|
|
sha2aText.set(this.authKeyUint8.subarray(x, x + 36), 16);
|
|
promises.push(CryptoWorker.invokeCrypto('sha256-hash', sha2aText));
|
|
|
|
sha2bText.set(this.authKeyUint8.subarray(40 + x, 40 + x + 36), 0);
|
|
sha2bText.set(msgKey, 36);
|
|
promises.push(CryptoWorker.invokeCrypto('sha256-hash', 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];
|
|
});
|
|
}
|
|
|
|
public isStopped() {
|
|
return networkerFactory.akStopped && !this.isFileNetworker;
|
|
}
|
|
|
|
private performScheduledRequest() {
|
|
// this.log('scheduled', this.dcId, this.iii)
|
|
|
|
if(this.isStopped()) {
|
|
return false;
|
|
}
|
|
|
|
if(this.pendingAcks.length) {
|
|
const ackMsgIds = 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 = 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 outMessage: MTPNetworker['sentMessages'][keyof MTPNetworker['sentMessages']];
|
|
const messages: typeof outMessage[] = [];
|
|
|
|
//const currentTime = Date.now();
|
|
let messagesByteLen = 0;
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
let hasApiCall = false;
|
|
let hasHttpWait = false;
|
|
/// #endif
|
|
|
|
let lengthOverflow = false;
|
|
|
|
// * Сюда никогда не попадут контейнеры, так как их не будет в pendingMessages
|
|
const keys = sortLongsArray(Object.keys(this.pendingMessages));
|
|
for(const messageId of keys) {
|
|
//const value = this.pendingMessages[messageId];
|
|
|
|
//if(!value || value <= currentTime) {
|
|
const message = this.sentMessages[messageId];
|
|
if(message && message.body) {
|
|
/* 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((messagesByteLen + messageByteLength) > 655360) { // 640 Kb
|
|
this.log.warn('lengthOverflow', message, messages);
|
|
lengthOverflow = true;
|
|
|
|
if(outMessage) { // if it's not a first message
|
|
break;
|
|
}
|
|
}
|
|
|
|
messages.push(message);
|
|
messagesByteLen += messageByteLength;
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
if(message.isAPI) {
|
|
hasApiCall = true;
|
|
} else if(message.longPoll) {
|
|
hasHttpWait = true;
|
|
}
|
|
/// #endif
|
|
|
|
outMessage = message;
|
|
} else {
|
|
// this.log(message, messageId)
|
|
}
|
|
|
|
delete this.pendingMessages[messageId];
|
|
//}
|
|
}
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
/// #if MTPROTO_HAS_WS
|
|
if(this.transport instanceof HTTP)
|
|
/// #endif
|
|
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(true)
|
|
});
|
|
}
|
|
/// #endif
|
|
|
|
if(!messages.length) {
|
|
// this.log('no scheduled messages')
|
|
return;
|
|
}
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
const noResponseMsgs: Array<string> = messages.filter(message => message.noResponse).map(message => message.msg_id);
|
|
/// #endif
|
|
|
|
if(messages.length > 1) {
|
|
const container = this.generateContainerMessage(messagesByteLen, messages);
|
|
outMessage = container.messageWithBody;
|
|
|
|
this.sentMessages[outMessage.msg_id] = container.message;
|
|
} else {
|
|
this.sentMessages[outMessage.msg_id] = outMessage;
|
|
}
|
|
|
|
this.pendingAcks = [];
|
|
|
|
const promise = this.sendEncryptedRequest(outMessage);
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
/// #if MTPROTO_HAS_WS
|
|
if(this.transport instanceof HTTP)
|
|
/// #endif
|
|
this.handleSentEncryptedRequestHTTP(promise, outMessage, noResponseMsgs);
|
|
/// #endif
|
|
|
|
/// #if MTPROTO_HAS_WS
|
|
/// #if MTPROTO_HAS_HTTP
|
|
if(!(this.transport instanceof HTTP))
|
|
/// #endif
|
|
this.cleanupSent(); // ! WARNING
|
|
/// #endif
|
|
|
|
if(lengthOverflow) {
|
|
this.scheduleRequest();
|
|
}
|
|
}
|
|
|
|
private generateContainerMessage(messagesByteLen: number, messages: MTMessage[]) {
|
|
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) => {
|
|
innerMessages.push(message.msg_id);
|
|
// this.log('Pushing to container:', message.msg_id);
|
|
container.storeLong(message.msg_id, 'CONTAINER[' + i + '][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]');
|
|
});
|
|
|
|
const message: MTMessage = {
|
|
msg_id: timeManager.generateId(),
|
|
seq_no: this.generateSeqNo(true),
|
|
container: true,
|
|
inner: innerMessages
|
|
};
|
|
|
|
if(Modes.debug/* || true */) {
|
|
this.log.warn('Container', innerMessages, message.msg_id, message.seq_no);
|
|
}
|
|
|
|
return {
|
|
message,
|
|
messageWithBody: Object.assign({body: container.getBytes(true)}, message),
|
|
};
|
|
}
|
|
|
|
private async getEncryptedMessage(dataWithPadding: Uint8Array) {
|
|
const msgKey = await this.getMsgKey(dataWithPadding, true);
|
|
const keyIv = await this.getAesKeyIv(msgKey, true);
|
|
// this.log('after msg key iv')
|
|
|
|
const encryptedBytes = await CryptoWorker.invokeCrypto('aes-encrypt', dataWithPadding, keyIv[0], keyIv[1]);
|
|
// this.log('Finish encrypt')
|
|
|
|
return {
|
|
bytes: encryptedBytes,
|
|
msgKey
|
|
};
|
|
}
|
|
|
|
private getDecryptedMessage(msgKey: Uint8Array, encryptedData: Uint8Array) {
|
|
// this.log('get decrypted start')
|
|
return this.getAesKeyIv(msgKey, false).then((keyIv) => {
|
|
// this.log('after msg key iv')
|
|
return CryptoWorker.invokeCrypto('aes-decrypt', encryptedData, keyIv[0], keyIv[1]);
|
|
});
|
|
}
|
|
|
|
private 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.getOffset() % 16)) + 16 * (1 + nextRandomUint(8) % 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 = bufferConcats(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;
|
|
});
|
|
}
|
|
|
|
private sendEncryptedRequest(message: MTMessage) {
|
|
return this.getEncryptedOutput(message).then(requestData => {
|
|
this.debug && this.log.debug('sendEncryptedRequest: launching message into space:', message, [message.msg_id].concat(message.inner || []), requestData.length);
|
|
const promise: Promise<Uint8Array> = this.transport.send(requestData) as any;
|
|
// this.debug && this.log.debug('sendEncryptedRequest: launched message into space:', message, promise);
|
|
|
|
/// #if !MTPROTO_HAS_HTTP
|
|
return promise;
|
|
/// #else
|
|
|
|
/// #if MTPROTO_HAS_WS
|
|
if(!(this.transport instanceof HTTP)) return promise;
|
|
/// #endif
|
|
|
|
const baseError = {
|
|
code: 406,
|
|
type: 'NETWORK_BAD_RESPONSE',
|
|
transport: this.transport
|
|
};
|
|
|
|
return promise.then((result) => {
|
|
if(!result?.byteLength) {
|
|
throw baseError;
|
|
}
|
|
|
|
// this.debug && this.log.debug('sendEncryptedRequest: got response for:', message, [message.msg_id].concat(message.inner || []));
|
|
return result;
|
|
}, (error) => {
|
|
if(!error.message && !error.type) {
|
|
error = Object.assign(baseError, {
|
|
type: 'NETWORK_BAD_REQUEST',
|
|
originalError: error
|
|
});
|
|
}
|
|
|
|
throw error;
|
|
});
|
|
/// #endif
|
|
});
|
|
}
|
|
|
|
public parseResponse(responseBuffer: Uint8Array) {
|
|
//const perf = performance.now();
|
|
/* if(this.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<MTLong>(dataWithPadding, {mtproto: true});
|
|
|
|
/* const salt = */deserializer.fetchIntBytes(64, true, 'salt'); // need
|
|
const sessionId = deserializer.fetchIntBytes(64, true, '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<MTLong>(/* 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 as Error).message, (e as Error).stack);
|
|
result.body = {
|
|
_: 'parse_error',
|
|
error: e
|
|
};
|
|
}
|
|
|
|
if(deserializer.getOffset() !== offset + result.bytes) {
|
|
// console.warn(dT(), 'set offset', this.offset, offset, result.bytes)
|
|
// this.log(result)
|
|
deserializer.setOffset(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
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
private applyServerSalt(newServerSalt: string) {
|
|
const serverSalt = longToBytes(newServerSalt);
|
|
|
|
sessionStorage.set({
|
|
['dc' + this.dcId + '_server_salt']: bytesToHex(serverSalt)
|
|
});
|
|
|
|
this.serverSalt = new Uint8Array(serverSalt);
|
|
}
|
|
|
|
// ! таймаут очень сильно тормозит скорость работы сокета (даже нулевой)
|
|
public scheduleRequest(delay?: number) {
|
|
/* if(!this.isOnline) {
|
|
return;
|
|
} */
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
/// #if MTPROTO_HAS_WS
|
|
if(this.transport instanceof HTTP) {
|
|
/// #endif
|
|
if(this.offline) {
|
|
this.checkConnection('forced schedule');
|
|
}
|
|
|
|
delay ||= 0; // set zero timeout to pack other messages too
|
|
/// #if MTPROTO_HAS_WS
|
|
}
|
|
/// #endif
|
|
/// #endif
|
|
|
|
const nextReq = Date.now() + (delay || 0);
|
|
if(this.nextReq && (delay === undefined || this.nextReq <= nextReq)) {
|
|
//this.debug && this.log('scheduleRequest: nextReq', this.nextReq, nextReq);
|
|
return;
|
|
}
|
|
|
|
//this.debug && this.log('scheduleRequest: delay', delay);
|
|
|
|
/* if(this.nextReqTimeout) {
|
|
return;
|
|
} */
|
|
|
|
//const perf = performance.now();
|
|
if(this.nextReqTimeout) {
|
|
clearTimeout(this.nextReqTimeout);
|
|
}
|
|
|
|
const cb = () => {
|
|
//this.debug && this.log('scheduleRequest: timeout delay was:', performance.now() - perf);
|
|
|
|
this.nextReqTimeout = 0;
|
|
this.nextReq = 0;
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
/// #if MTPROTO_HAS_WS
|
|
if(this.transport instanceof HTTP)
|
|
/// #endif
|
|
if(this.offline) {
|
|
//this.log('Cancel scheduled');
|
|
return;
|
|
}
|
|
/// #endif
|
|
|
|
this.performScheduledRequest();
|
|
};
|
|
|
|
this.nextReq = nextReq;
|
|
|
|
if(delay !== undefined) {
|
|
this.nextReqTimeout = self.setTimeout(cb, delay);
|
|
} else {
|
|
cb();
|
|
}
|
|
}
|
|
|
|
private ackMessage(msgId: MTLong) {
|
|
// this.log('ack message', msgID)
|
|
this.pendingAcks.push(msgId);
|
|
|
|
let delay: number;
|
|
|
|
/// #if MTPROTO_HAS_HTTP
|
|
/// #if MTPROTO_HAS_WS
|
|
if(this.transport instanceof HTTP)
|
|
/// #endif
|
|
delay = 30000;
|
|
/// #endif
|
|
|
|
this.scheduleRequest(delay);
|
|
}
|
|
|
|
private reqResendMessage(msgId: MTLong) {
|
|
if(this.debug) {
|
|
this.log.debug('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;
|
|
}
|
|
|
|
private processMessageAck(messageId: Long) {
|
|
const sentMessage = this.sentMessages[messageId];
|
|
if(sentMessage && !sentMessage.acked) {
|
|
//delete sentMessage.body;
|
|
sentMessage.acked = true;
|
|
}
|
|
}
|
|
|
|
private 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
|
|
};
|
|
}
|
|
|
|
/**
|
|
* * только для сокета
|
|
* TODO: consider about containers resend
|
|
*/
|
|
public resend() {
|
|
for(const id in this.sentMessages) {
|
|
const msg = this.sentMessages[id];
|
|
if(msg.body || msg.container) {
|
|
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: MTLong, sessionId: Uint8Array | number[]) {
|
|
if(message._ === 'messageEmpty') {
|
|
this.log.warn('processMessage: messageEmpty', message, messageId);
|
|
return;
|
|
}
|
|
|
|
// messageId = messageId.toString();
|
|
|
|
const msgidInt = parseInt(messageId.substr(0, -10), 10);
|
|
if(msgidInt % 2) {
|
|
this.log.warn('Server even message id: ', messageId, message);
|
|
return;
|
|
}
|
|
|
|
/* if(this.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);
|
|
|
|
switch(message.error_code) {
|
|
case 16: // * msg_id too low
|
|
case 17: // * msg_id too high
|
|
case 32: // * msg_seqno too low
|
|
case 33: // * msg_seqno too high
|
|
case 64: { // * invalid container
|
|
//const changedOffset = timeManager.applyServerTime(bigStringInt(messageId).shiftRight(32).toString(10));
|
|
const bigInt = str2bigInt(messageId, 10);
|
|
rightShift_(bigInt, 32);
|
|
const changedOffset = timeManager.applyServerTime(+bigInt2str(bigInt, 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);
|
|
}
|
|
|
|
// * invalid container
|
|
/* case 64: {
|
|
const badMessage = this.sentMessages[message.bad_msg_id];
|
|
if(badMessage) {
|
|
for(const msgId of badMessage.inner) {
|
|
if(this.sentMessages[msgId] !== undefined) {
|
|
this.updateSentMessage
|
|
}
|
|
}
|
|
const inner = badMessage.inner;
|
|
}
|
|
} */
|
|
}
|
|
|
|
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(this.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);
|
|
} */
|
|
|
|
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(this.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 don't require acknowledgments
|
|
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(this.debug) {
|
|
this.log.debug('Update', message);
|
|
} */
|
|
|
|
if(networkerFactory.updatesProcessor !== null) {
|
|
networkerFactory.updatesProcessor(message);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|