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.

1358 lines
42 KiB

import {isObject} from './bin_utils';
import { bigStringInt} from './bin_utils';
import {TLDeserialization, TLSerialization} from './tl_utils';
import CryptoWorker from '../crypto/cryptoworker';
import AppStorage from '../storage';
import Schema from './schema';
import timeManager from './timeManager';
import NetworkerFactory from './networkerFactory';
4 years ago
import { logger, LogLevels } from '../logger';
import { Modes, App } 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 } from '../../helpers/random';
/// #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,
singleInRequest?: true,
longPoll?: true,
noResponse?: true, // only with http (http_wait for longPoll)
};
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: Array<number> = [];
private sessionID: Array<number> = [];
private lastResendReq: {
req_msg_id: string,
resend_msg_ids: Array<string>
} | null = null;
//private transport: MTTransport;
private name: string;
private log: ReturnType<typeof logger>;
private isOnline = false;
//public onConnectionStatusChange: (online: boolean) => void;
constructor(public dcID: number, private authKey: number[], private authKeyID: Uint8Array,
private serverSalt: number[], private transport: MTTransport, options: InvokeApiOptions = {}) {
this.authKeyUint8 = convertToUint8Array(this.authKey);
//this.authKeyID = sha1BytesSync(this.authKey).slice(-8);
//console.trace('Create', dcID, options);
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);
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
}
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) {
const newInner: string[] = [];
sentMessage.inner.forEach((innerSentMessageID) => {
const innerSentMessage = this.updateSentMessage(innerSentMessageID);
if(innerSentMessage) {
newInner.push(innerSentMessage.msg_id);
}
});
sentMessage.inner = newInner;
}
sentMessage.msg_id = timeManager.generateID();
sentMessage.seq_no = this.generateSeqNo(sentMessage.notContentRelated || sentMessage.container);
this.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()
};
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()
};
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!');
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 {
this.log('Api call', method, params, options);
}
return this.pushMessage(message, options);
}
/// #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;
}
AppStorage.get<number>('dc').then((baseDcID: number) => {
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);
document.body.addEventListener('online', this.checkConnection, false);
document.body.addEventListener('focus', this.checkConnection, false);
} else {
this.checkLongPoll();
this.scheduleRequest();
document.body.removeEventListener('online', this.checkConnection);
document.body.removeEventListener('focus', this.checkConnection);
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
4 years ago
// тут можно сделать таймаут и выводить дисконнект
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(() => {
this.log.error('timeout', message);
this.setConnectionStatus(false);
}, 5e3);
promise.finally(() => {
clearTimeout(timeout);
this.setConnectionStatus(true);
});
}
return promise;
}
public setConnectionStatus(online: boolean) {
const willChange = this.isOnline != online;
this.isOnline = online;
if(willChange && 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.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;
}
this.log('Resend', messageID, sentMessage, this.pendingMessages);
this.scheduleRequest(delay);
}
public async getMsgKey(dataWithPadding: ArrayBuffer, isOut: boolean) {
const authKey = this.authKeyUint8;
const x = isOut ? 0 : 8
const msgKeyLargePlain = bufferConcat(authKey.subarray(88 + x, 88 + x + 32), dataWithPadding);
const msgKeyLarge = await CryptoWorker.sha256Hash(msgKeyLargePlain);
const msgKey = new Uint8Array(msgKeyLarge).subarray(8, 24);
return msgKey;
};
public getAesKeyIv(msgKey: Uint8Array | number[], isOut: boolean): Promise<[Uint8Array, Uint8Array]> {
const authKey = this.authKeyUint8;
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(authKey.subarray(x, x + 36), 16);
promises.push(CryptoWorker.sha256Hash(sha2aText));
sha2bText.set(authKey.subarray(40 + x, 40 + x + 36), 0);
sha2bText.set(msgKey, 36);
promises.push(CryptoWorker.sha256Hash(sha2bText));
return Promise.all(promises).then((results) => {
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 performScheduledRequest = () => {
// this.log('scheduled', this.dcID, this.iii)
/// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD
if(this.offline) {
this.log('Cancel scheduled');
return false;
}
this.nextReq = 0;
/// #endif
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;
let singlesCount = 0;
for(const messageID in this.pendingMessages) {
const value = this.pendingMessages[messageID];
if(!value || value >= currentTime) {
if(message = this.sentMessages[messageID]) {
//this.log('performScheduledRequest message:', message);
const messageByteLength = (/* message.body.byteLength || */message.body.length) + 32;
if(!message.notContentRelated &&
lengthOverflow) {
continue; // maybe break here
}
if(!message.notContentRelated &&
messagesByteLen &&
messagesByteLen + messageByteLength > 655360) { // 640 Kb
this.log.warn('lengthOverflow', message);
lengthOverflow = true;
continue; // maybe break here
}
if(message.singleInRequest) {
singlesCount++;
if(singlesCount > 1) {
continue; // maybe break here
}
}
messages.push(message);
messagesByteLen += messageByteLength;
if(message.isAPI) {
hasApiCall = true;
} else if(message.longPoll) {
hasHttpWait = true;
}
} else {
// this.log(message, messageID)
}
delete 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 || true) {
this.log('Container', innerMessages, message.msg_id, message.seq_no);
}
} else {
if(message.noResponse) {
noResponseMsgs.push(message.msg_id);
}
this.sentMessages[message.msg_id] = message;
}
this.pendingAcks = [];
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 || singlesCount > 1) {
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: msgKey
};
}
public getDecryptedMessage(msgKey: Uint8Array | number[], encryptedData: Uint8Array | number[]): Promise<ArrayBuffer> {
// this.log('get decrypted start')
return this.getAesKeyIv(msgKey, false).then((keyIv) => {
// this.log('after msg key iv')
return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]);
});
}
public sendEncryptedRequest(message: MTMessage) {
this.log.debug('Send encrypted', message, this.authKeyID);
// console.trace()
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 dataBuffer = data.getBuffer();
const paddingLength = (16 - (data.offset % 16)) + 16 * (1 + nextRandomInt(5));
const padding = [...new Uint8Array(paddingLength).randomize()];
//MTProto.secureRandom.nextBytes(padding);
const dataWithPadding = bufferConcat(dataBuffer, padding);
// this.log('Adding padding', dataBuffer, padding, dataWithPadding)
// this.log('auth_key_id', bytesToHex(self.authKeyID))
/* if(message.fileUpload) {
this.log('Send encrypted: body length:', (message.body as ArrayBuffer).byteLength, paddingLength, dataWithPadding);
} */
return this.getEncryptedMessage(dataWithPadding).then((encryptedResult) => {
this.log.debug('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');
//var requestData = xhrSendBuffer ? request.getBuffer() : request.getBytes(true) as Uint8Array;
const requestData = request.getBytes(true);
const baseError = {
code: 406,
type: 'NETWORK_BAD_RESPONSE',
transport: this.transport
};
/* if(message.fileUpload) {
this.log('Send encrypted: requestData length:', requestData.length, requestData.length % 16, paddingLength % 16, paddingLength, data.offset);
} */
const promise = this.transport.send(requestData);
/// #if !MTPROTO_HTTP && !MTPROTO_HTTP_UPLOAD
/* if(!(this.transport instanceof HTTP)) */ return promise;
/// #else
if(!(this.transport instanceof HTTP)) return promise;
return promise.then((result) => {
if(!result || !result.byteLength) {
return Promise.reject(baseError);
}
return result;
}, (error) => {
if(!error.message && !error.type) {
error = Object.assign(baseError, {
type: 'NETWORK_BAD_REQUEST',
originalError: error
});
}
return Promise.reject(error);
});
/// #endif
});
}
public parseResponse(responseBuffer: Uint8Array) {
this.log.debug('Start parsing response'/* , responseBuffer */);
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');
return {
response,
messageID,
sessionID,
seqNo
};
});
});
}
public applyServerSalt(newServerSalt: string) {
const serverSalt = longToBytes(newServerSalt);
AppStorage.set({
['dc' + this.dcID + '_server_salt']: bytesToHex(serverSalt)
});
this.serverSalt = 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;
} */
var nextReq = Date.now() + delay;
if(delay && this.nextReq && this.nextReq <= nextReq) {
return false;
}
// this.log('schedule req', delay)
// console.trace()
clearTimeout(this.nextReqTimeout);
this.nextReqTimeout = 0;
if(delay > 0) {
this.nextReqTimeout = self.setTimeout(this.performScheduledRequest, delay || 0);
} else {
setTimeout(this.performScheduledRequest, 0);
}
this.nextReq = nextReq;
/// #endif
}
public ackMessage(msgID: string) {
// this.log('ack message', msgID)
this.pendingAcks.push(msgID);
this.scheduleRequest(30000);
}
public reqResendMessage(msgID: string) {
this.log('Req resend', msgID);
this.pendingResends.push(msgID);
this.scheduleRequest(100);
}
public cleanupSent() {
let notEmpty = false;
// this.log('clean start', this.dcID/*, this.sentMessages*/)
Object.keys(this.sentMessages).forEach((msgID) => {
const message = this.sentMessages[msgID];
// this.log('clean iter', msgID, message)
if(message.notContentRelated && this.pendingMessages[msgID] === undefined) {
// this.log('clean notContentRelated', msgID)
delete this.sentMessages[msgID];
} else if(message.container) {
for(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;
return true;
}
return false;
}
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;
}
this.log.debug('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);
this.log.debug('new_session_created', message);
//this.updateSession();
this.processMessageAck(message.first_msg_id);
this.applyServerSalt(message.server_salt);
AppStorage.get<number>('dc').then((baseDcID: number) => {
if(baseDcID == this.dcID && !this.isFileNetworker && NetworkerFactory.updatesProcessor) {
NetworkerFactory.updatesProcessor(message, true);
}
});
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(Modes.debug) {
this.log.debug('Rpc response', message.result);
} else {
let dRes = message.result._;
if(!dRes) {
if(message.result.length > 5) {
dRes = '[..' + message.result.length + '..]';
} else {
dRes = message.result;
}
}
this.log.debug('Rpc response', dRes, sentMessage);
}
sentMessage.deferred.resolve(message.result);
}
if(sentMessage.isAPI && !this.connectionInited) {
this.connectionInited = true;
////this.log('Rpc set connectionInited to:', this.connectionInited);
}
}
delete this.sentMessages[sentMessageID];
} else {
this.log('Rpc result for unknown message:', sentMessageID);
}
break;
}
default:
this.ackMessage(messageID);
this.log.debug('Update', message);
if(NetworkerFactory.updatesProcessor !== null) {
NetworkerFactory.updatesProcessor(message, true);
}
break;
}
}
}