Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1528 lines
48 KiB

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<string> = [];
private sentMessages: {
[msgId: string]: MTMessage
} = {};
private pendingMessages: {[msgId: string]: number} = {};
private pendingAcks: Array<string> = [];
private pendingResends: Array<string> = [];
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<string>
} | null = null;
private name: string;
private log: ReturnType<typeof logger>;
public isOnline = false;
private lastResponseTime = 0;
private disconnectDelay: number;
private pingPromise: CancellablePromise<any>;
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<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();
});
};
/// #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<MTPNetworker['sendEncryptedRequest']>, 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<Promise<number[]>> = [];
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<string> = 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<string> = 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<string> = [];
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<ArrayBuffer> {
// this.log('get decrypted start')
return this.getAesKeyIv(msgKey, false).then((keyIv) => {
// this.log('after msg key iv')
return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]);
});
}
public 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;
}
}
}