Telegram Web, preconfigured for usage in I2P. http://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.
 
 
 
 
 
 

1545 lines
49 KiB

/*!
* Webogram v0.3.2 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
angular.module('izhukov.mtproto', ['izhukov.utils'])
.factory('MtpDcConfigurator', function () {
var dcOptions = Config.Modes.test
? [
{id: 1, url: 'http://173.240.5.253'},
{id: 2, url: 'http://149.154.167.40'},
{id: 3, url: 'http://174.140.142.5'}
]
: (location.protocol == 'https:'
? [
{id: 1, url: 'https://pluto.web.telegram.org'},
{id: 2, url: 'https://venus.web.telegram.org'},
{id: 3, url: 'https://aurora.web.telegram.org'},
{id: 4, url: 'https://vesta.web.telegram.org'},
{id: 5, url: 'https://flora.web.telegram.org'}
]
: [
{id: 1, url: 'http://173.240.5.1'},
{id: 2, url: 'http://149.154.167.51'},
{id: 3, url: 'http://174.140.142.6'},
{id: 4, url: 'http://149.154.167.91'},
{id: 5, url: 'http://149.154.171.5'}
]);
var sslSubdomains = 'pluto,venus,aurora,vesta,flora';
var chosenServers = {};
function chooseServer(dcID) {
if (chosenServers[dcID] === undefined) {
var chosenServer = false,
i, dcOption;
for (i = 0; i < dcOptions.length; i++) {
dcOption = dcOptions[i];
if (dcOption.id == dcID) {
chosenServer = dcOption.url;
}
}
chosenServers[dcID] = chosenServer;
}
return chosenServers[dcID];
}
return {
chooseServer: chooseServer
};
})
.factory('MtpRsaKeysManager', function () {
/**
* Server public key, obtained from here: https://core.telegram.org/api/obtaining_api_id
*
* -----BEGIN RSA PUBLIC KEY-----
* MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
* lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
* an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
* Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
* 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
* Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
* -----END RSA PUBLIC KEY-----
*/
var publisKeysHex = [{
modulus: 'c150023e2f70db7985ded064759cfecf0af328e69a41daf4d6f01b538135a6f91f8f8b2a0ec9ba9720ce352efcf6c5680ffc424bd634864902de0b4bd6d49f4e580230e3ae97d95c8b19442b3c0a10d8f5633fecedd6926a7f6dab0ddb7d457f9ea81b8465fcd6fffeed114011df91c059caedaf97625f6c96ecc74725556934ef781d866b34f011fce4d835a090196e9a5f0e4449af7eb697ddb9076494ca5f81104a305b6dd27665722c46b60e5df680fb16b210607ef217652e60236c255f6a28315f4083a96791d7214bf64c1df4fd0db1944fb26a2a57031b32eee64ad15a8ba68885cde74a5bfc920f6abf59ba5c75506373e7130f9042da922179251f',
exponent: '010001'
}];
var publicKeysParsed = {};
var prepared = false;
function prepareRsaKeys () {
if (prepared) {
return;
}
for (var i = 0; i < publisKeysHex.length; i++) {
var keyParsed = publisKeysHex[i];
var RSAPublicKey = new TLSerialization();
RSAPublicKey.storeBytes(bytesFromHex(keyParsed.modulus), 'n');
RSAPublicKey.storeBytes(bytesFromHex(keyParsed.exponent), 'e');
var buffer = RSAPublicKey.getBuffer();
var fingerprintBytes = sha1BytesSync(buffer).slice(-8);
fingerprintBytes.reverse();
publicKeysParsed[bytesToHex(fingerprintBytes)] = {
modulus: keyParsed.modulus,
exponent: keyParsed.exponent
};
}
prepared = true;
};
function selectRsaKeyByFingerPrint (fingerprints) {
prepareRsaKeys();
var fingerprintHex, foundKey, i;
for (i = 0; i < fingerprints.length; i++) {
fingerprintHex = bigStringInt(fingerprints[i]).toString(16);
if (foundKey = publicKeysParsed[fingerprintHex]) {
return angular.extend({fingerprint: fingerprints[i]}, foundKey);
}
}
return false;
};
return {
prepare: prepareRsaKeys,
select: selectRsaKeyByFingerPrint
};
})
.service('MtpSecureRandom', function () {
return new SecureRandom();
})
.factory('MtpTimeManager', function (Storage) {
var lastMessageID = [0, 0],
timeOffset = 0;
Storage.get('server_time_offset').then(function (to) {
if (to) {
timeOffset = to;
}
});
function generateMessageID () {
var timeTicks = tsNow(),
timeSec = Math.floor(timeTicks / 1000) + timeOffset,
timeMSec = timeTicks % 1000,
random = nextRandomInt(0xFFFF);
var messageID = [timeSec, (timeMSec << 21) | (random << 3) | 4];
if (lastMessageID[0] > messageID[0] ||
lastMessageID[0] == messageID[0] && lastMessageID[1] >= messageID[1]) {
messageID = [lastMessageID[0], lastMessageID[1] + 4];
}
lastMessageID = messageID;
// console.log('generated msg id', messageID);
return longFromInts(messageID[0], messageID[1]);
};
function applyServerTime (serverTime, localTime) {
var newTimeOffset = serverTime - Math.floor((localTime || tsNow()) / 1000),
changed = Math.abs(timeOffset - newTimeOffset) > 10;
Storage.set({server_time_offset: newTimeOffset});
lastMessageID = [0, 0];
timeOffset = newTimeOffset;
console.log(dT(), 'Apply server time', serverTime, localTime, newTimeOffset, changed);
return changed;
};
return {
generateID: generateMessageID,
applyServerTime: applyServerTime,
};
})
.factory('MtpAuthorizer', function (MtpDcConfigurator, MtpRsaKeysManager, MtpSecureRandom, MtpTimeManager, CryptoWorker, $http, $q, $timeout) {
var chromeMatches = navigator.userAgent.match(/Chrome\/(\d+(\.\d+)?)/),
chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false,
xhrSendBuffer = !('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30);
delete $http.defaults.headers.post['Content-Type'];
delete $http.defaults.headers.common['Accept'];
function mtpSendPlainRequest (dcID, requestBuffer) {
var requestLength = requestBuffer.byteLength,
requestArray = new Int32Array(requestBuffer);
var header = new TLSerialization();
header.storeLongP(0, 0, 'auth_key_id'); // Auth key
header.storeLong(MtpTimeManager.generateID(), 'msg_id'); // Msg_id
header.storeInt(requestLength, 'request_length');
var headerBuffer = header.getBuffer(),
headerArray = new Int32Array(headerBuffer),
headerLength = headerBuffer.byteLength;
var resultBuffer = new ArrayBuffer(headerLength + requestLength),
resultArray = new Int32Array(resultBuffer);
resultArray.set(headerArray);
resultArray.set(requestArray, headerArray.length);
var requestData = xhrSendBuffer ? resultBuffer : resultArray,
requestPromise;
try {
requestPromise = $http.post(MtpDcConfigurator.chooseServer(dcID) + '/apiw1', requestData, {
responseType: 'arraybuffer',
transformRequest: null
});
} catch (e) {
requestPromise = $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE', originalError: e});
}
return requestPromise.then(
function (result) {
if (!result.data || !result.data.byteLength) {
return $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE'});
}
try {
var deserializer = new TLDeserialization(result.data, {mtproto: true});
var auth_key_id = deserializer.fetchLong('auth_key_id');
var msg_id = deserializer.fetchLong('msg_id');
var msg_len = deserializer.fetchInt('msg_len');
} catch (e) {
return $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE', originalError: e});
}
rng_seed_time();
return deserializer;
},
function (error) {
if (!error.message && !error.type) {
error = {code: 406, type: 'NETWORK_BAD_REQUEST', originalError: error};
}
return $q.reject(error);
}
);
};
function mtpSendReqPQ (auth) {
var deferred = auth.deferred;
var request = new TLSerialization({mtproto: true});
request.storeMethod('req_pq', {nonce: auth.nonce});
console.log(dT(), 'Send req_pq', bytesToHex(auth.nonce));
mtpSendPlainRequest(auth.dcID, request.getBuffer()).then(function (deserializer) {
var response = deserializer.fetchObject('ResPQ');
if (response._ != 'resPQ') {
throw new Error('resPQ response invalid: ' + response._);
}
if (!bytesCmp (auth.nonce, response.nonce)) {
throw new Error('resPQ nonce mismatch');
}
auth.serverNonce = response.server_nonce;
auth.pq = response.pq;
auth.fingerprints = response.server_public_key_fingerprints;
console.log(dT(), 'Got ResPQ', bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints);
auth.publicKey = MtpRsaKeysManager.select(auth.fingerprints);
if (!auth.publicKey) {
throw new Error('No public key found');
}
console.log(dT(), 'PQ factorization start', auth.pq);
CryptoWorker.factorize(auth.pq).then(function (pAndQ) {
auth.p = pAndQ[0];
auth.q = pAndQ[1];
console.log(dT(), 'PQ factorization done', pAndQ[2]);
mtpSendReqDhParams(auth);
}, function (error) {
console.log('Worker error', error, error.stack);
deferred.reject(error);
});
}, function (error) {
console.log(dT(), 'req_pq error', error.message);
deferred.reject(error);
});
$timeout(function () {
MtpRsaKeysManager.prepare();
});
};
function mtpSendReqDhParams (auth) {
var deferred = auth.deferred;
auth.newNonce = new Array(32);
MtpSecureRandom.nextBytes(auth.newNonce);
var data = new TLSerialization({mtproto: true});
data.storeObject({
_: 'p_q_inner_data',
pq: auth.pq,
p: auth.p,
q: auth.q,
nonce: auth.nonce,
server_nonce: auth.serverNonce,
new_nonce: auth.newNonce
}, 'P_Q_inner_data', 'DECRYPTED_DATA');
var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes());
var request = new TLSerialization({mtproto: true});
request.storeMethod('req_DH_params', {
nonce: auth.nonce,
server_nonce: auth.serverNonce,
p: auth.p,
q: auth.q,
public_key_fingerprint: auth.publicKey.fingerprint,
encrypted_data: rsaEncrypt(auth.publicKey, dataWithHash)
});
console.log(dT(), 'Send req_DH_params');
mtpSendPlainRequest(auth.dcID, request.getBuffer()).then(function (deserializer) {
var response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE');
if (response._ != 'server_DH_params_fail' && response._ != 'server_DH_params_ok') {
deferred.reject(new Error('Server_DH_Params response invalid: ' + response._));
return false;
}
if (!bytesCmp (auth.nonce, response.nonce)) {
deferred.reject(new Error('Server_DH_Params nonce mismatch'));
return false;
}
if (!bytesCmp (auth.serverNonce, response.server_nonce)) {
deferred.reject(new Error('Server_DH_Params server_nonce mismatch'));
return false;
}
if (response._ == 'server_DH_params_fail') {
var newNonceHash = sha1BytesSync(auth.newNonce).slice(-16);
if (!bytesCmp (newNonceHash, response.new_nonce_hash)) {
deferred.reject(new Error('server_DH_params_fail new_nonce_hash mismatch'));
return false;
}
deferred.reject(new Error('server_DH_params_fail'));
return false;
}
try {
mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer);
} catch (e) {
deferred.reject(e);
return false;
}
mtpSendSetClientDhParams(auth);
}, function (error) {
deferred.reject(error);
});
};
function mtpDecryptServerDhDataAnswer (auth, encryptedAnswer) {
auth.localTime = tsNow();
auth.tmpAesKey = sha1BytesSync(auth.newNonce.concat(auth.serverNonce)).concat(sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(0, 12));
auth.tmpAesIv = sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(12).concat(sha1BytesSync([].concat(auth.newNonce, auth.newNonce)), auth.newNonce.slice(0, 4));
var answerWithHash = aesDecryptSync(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv);
var hash = answerWithHash.slice(0, 20);
var answerWithPadding = answerWithHash.slice(20);
var buffer = bytesToArrayBuffer(answerWithPadding);
var deserializer = new TLDeserialization(buffer, {mtproto: true});
var response = deserializer.fetchObject('Server_DH_inner_data');
if (response._ != 'server_DH_inner_data') {
throw new Error('server_DH_inner_data response invalid: ' + constructor);
}
if (!bytesCmp (auth.nonce, response.nonce)) {
throw new Error('server_DH_inner_data nonce mismatch');
}
if (!bytesCmp (auth.serverNonce, response.server_nonce)) {
throw new Error('server_DH_inner_data serverNonce mismatch');
}
console.log(dT(), 'Done decrypting answer');
auth.g = response.g;
auth.dhPrime = response.dh_prime;
auth.gA = response.g_a;
auth.serverTime = response.server_time;
auth.retry = 0;
var offset = deserializer.getOffset();
if (!bytesCmp(hash, sha1BytesSync(answerWithPadding.slice(0, offset)))) {
throw new Error('server_DH_inner_data SHA1-hash mismatch');
}
MtpTimeManager.applyServerTime(auth.serverTime, auth.localTime);
};
function mtpSendSetClientDhParams(auth) {
var deferred = auth.deferred,
gBytes = bytesFromHex(auth.g.toString(16));
auth.b = new Array(256);
MtpSecureRandom.nextBytes(auth.b);
CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime).then(function (gB) {
var data = new TLSerialization({mtproto: true});
data.storeObject({
_: 'client_DH_inner_data',
nonce: auth.nonce,
server_nonce: auth.serverNonce,
retry_id: [0, auth.retry++],
g_b: gB,
}, 'Client_DH_Inner_Data');
var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes());
var encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv);
var request = new TLSerialization({mtproto: true});
request.storeMethod('set_client_DH_params', {
nonce: auth.nonce,
server_nonce: auth.serverNonce,
encrypted_data: encryptedData
});
console.log(dT(), 'Send set_client_DH_params');
mtpSendPlainRequest(auth.dcID, request.getBuffer()).then(function (deserializer) {
var response = deserializer.fetchObject('Set_client_DH_params_answer');
if (response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') {
deferred.reject(new Error('Set_client_DH_params_answer response invalid: ' + response._));
return false;
}
if (!bytesCmp (auth.nonce, response.nonce)) {
deferred.reject(new Error('Set_client_DH_params_answer nonce mismatch'));
return false
}
if (!bytesCmp (auth.serverNonce, response.server_nonce)) {
deferred.reject(new Error('Set_client_DH_params_answer server_nonce mismatch'));
return false;
}
CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime).then(function (authKey) {
var authKeyHash = sha1BytesSync(authKey),
authKeyAux = authKeyHash.slice(0, 8),
authKeyID = authKeyHash.slice(-8);
console.log(dT(), 'Got Set_client_DH_params_answer', response._);
switch (response._) {
case 'dh_gen_ok':
var newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16);
if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) {
deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash1 mismatch'));
return false;
}
var serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8));
// console.log('Auth successfull!', authKeyID, authKey, serverSalt);
auth.authKeyID = authKeyID;
auth.authKey = authKey;
auth.serverSalt = serverSalt;
deferred.resolve(auth);
break;
case 'dh_gen_retry':
var newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16);
if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) {
deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash2 mismatch'));
return false;
}
return mtpSendSetClientDhParams(auth);
case 'dh_gen_fail':
var newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16);
if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) {
deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash3 mismatch'));
return false;
}
deferred.reject(new Error('Set_client_DH_params_answer fail'));
return false;
}
}, function (error) {
deferred.reject(error);
})
}, function (error) {
deferred.reject(error);
});
}, function (error) {
deferred.reject(error);
})
};
var cached = {};
function mtpAuth (dcID) {
if (cached[dcID] !== undefined) {
return cached[dcID];
}
var nonce = [];
for (var i = 0; i < 16; i++) {
nonce.push(nextRandomInt(0xFF));
}
if (!MtpDcConfigurator.chooseServer(dcID)) {
return $q.reject(new Error('No server found for dc ' + dcID));
}
var auth = {
dcID: dcID,
nonce: nonce,
deferred: $q.defer()
};
$timeout(function () {
mtpSendReqPQ(auth);
});
cached[dcID] = auth.deferred.promise;
cached[dcID]['catch'](function () {
delete cached[dcID];
})
return cached[dcID];
};
return {
auth: mtpAuth
};
})
.factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpTimeManager, MtpSecureRandom, Storage, CryptoWorker, AppRuntimeManager, $http, $q, $timeout, $interval, $rootScope) {
var updatesProcessor,
iii = 0,
offline,
offlineInited = false,
chromeMatches = navigator.userAgent.match(/Chrome\/(\d+(\.\d+)?)/),
chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false,
xhrSendBuffer = !('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30);
delete $http.defaults.headers.post['Content-Type'];
delete $http.defaults.headers.common['Accept'];
$rootScope.retryOnline = function () {
$(document.body).trigger('online');
}
function MtpNetworker(dcID, authKey, serverSalt, options) {
options = options || {};
this.dcID = dcID;
this.iii = iii++;
this.authKey = authKey;
this.authKeyUint8 = convertToUint8Array(authKey);
this.authKeyBuffer = convertToArrayBuffer(authKey);
this.authKeyID = sha1BytesSync(authKey).slice(-8);
this.serverSalt = serverSalt;
this.upload = options.fileUpload || options.fileDownload || false;
this.updateSession();
this.currentRequests = 0;
this.checkConnectionPeriod = 0;
this.sentMessages = {};
this.serverMessages = [];
this.clientMessages = [];
this.pendingMessages = {};
this.pendingAcks = [];
this.pendingResends = [];
this.connectionInited = false;
this.pendingTimeouts = [];
this.longPollInt = $interval(this.checkLongPoll.bind(this), 10000);
this.checkLongPoll();
if (!offlineInited) {
offlineInited = true;
$rootScope.offline = true;
$rootScope.offlineConnecting = true;
}
if (Config.Navigator.mobile) {
this.setupMobileSleep();
}
};
MtpNetworker.prototype.updateSession = function () {
this.seqNo = 0;
this.sessionID = new Array(8);
MtpSecureRandom.nextBytes(this.sessionID);
if (false) {
this.sessionID[0] = 0xAB;
this.sessionID[1] = 0xCD;
}
};
MtpNetworker.prototype.setupMobileSleep = function () {
var self = this;
$rootScope.$watch('idle.isIDLE', function (isIDLE) {
if (isIDLE) {
self.sleepAfter = tsNow() + 30000;
} else {
delete self.sleepAfter;
self.checkLongPoll();
}
});
$rootScope.$on('push_received', function () {
// console.log(dT(), 'push recieved', self.sleepAfter);
if (self.sleepAfter) {
self.sleepAfter = tsNow() + 30000;
self.checkLongPoll();
}
});
};
MtpNetworker.prototype.updateSentMessage = function (sentMessageID) {
var sentMessage = this.sentMessages[sentMessageID];
if (!sentMessage) {
return false;
}
var self = this;
if (sentMessage.container) {
var newInner = [];
angular.forEach(sentMessage.inner, function(innerSentMessageID){
var innerSentMessage = self.updateSentMessage(innerSentMessageID);
if (innerSentMessage) {
newInner.push(innerSentMessage.msg_id);
}
});
sentMessage.inner = newInner;
}
sentMessage.msg_id = MtpTimeManager.generateID();
sentMessage.seq_no = this.generateSeqNo(
sentMessage.notContentRelated ||
sentMessage.container
);
this.sentMessages[sentMessage.msg_id] = sentMessage;
delete self.sentMessages[sentMessageID];
return sentMessage;
};
MtpNetworker.prototype.generateSeqNo = function (notContentRelated) {
var seqNo = this.seqNo * 2;
if (!notContentRelated) {
seqNo++;
this.seqNo++;
}
return seqNo;
}
MtpNetworker.prototype.wrapMtpCall = function (method, params, options) {
var serializer = new TLSerialization({mtproto: true});
serializer.storeMethod(method, params);
var messageID = MtpTimeManager.generateID(),
seqNo = this.generateSeqNo(),
message = {
msg_id: messageID,
seq_no: seqNo,
body: serializer.getBytes()
};
if (Config.Modes.debug) {
console.log(dT(), 'MT call', method, params, messageID, seqNo);
}
return this.pushMessage(message, options);
};
MtpNetworker.prototype.wrapMtpMessage = function (object, options) {
options = options || {};
var serializer = new TLSerialization({mtproto: true});
serializer.storeObject(object, 'Object');
var messageID = MtpTimeManager.generateID(),
seqNo = this.generateSeqNo(options.notContentRelated),
message = {
msg_id: messageID,
seq_no: seqNo,
body: serializer.getBytes()
};
if (Config.Modes.debug) {
console.log(dT(), 'MT message', object, messageID, seqNo);
}
return this.pushMessage(message, options);
};
MtpNetworker.prototype.wrapApiCall = function (method, params, options) {
var serializer = new TLSerialization(options);
if (!this.connectionInited) {
serializer.storeInt(0x1c900537, 'invokeWithLayer18');
serializer.storeInt(0x69796de9, 'initConnection');
serializer.storeInt(Config.App.id, 'api_id');
serializer.storeString(navigator.userAgent || 'Unknown UserAgent', 'device_model');
serializer.storeString(navigator.platform || 'Unknown Platform', 'system_version');
serializer.storeString(Config.App.version, 'app_version');
serializer.storeString(navigator.language || 'en', 'lang_code');
}
if (options.afterMessageID) {
serializer.storeInt(0xcb9f372d, 'invokeAfterMsg');
serializer.storeLong(options.afterMessageID, 'msg_id');
}
options.resultType = serializer.storeMethod(method, params);
var messageID = MtpTimeManager.generateID(),
seqNo = this.generateSeqNo(),
message = {
msg_id: messageID,
seq_no: seqNo,
body: serializer.getBytes(true),
isAPI: true
};
if (Config.Modes.debug) {
console.log(dT(), 'Api call', method, params, messageID, seqNo, options);
} else {
console.log(dT(), 'Api call', method);
}
return this.pushMessage(message, options);
};
MtpNetworker.prototype.checkLongPoll = function(force) {
var isClean = this.cleanupSent();
// console.log('Check lp', this.longPollPending, tsNow(), this.dcID, isClean);
if (this.longPollPending && tsNow() < this.longPollPending || this.offline) {
return false;
}
var self = this;
Storage.get('dc').then(function (baseDcID) {
if (isClean && (
baseDcID != self.dcID ||
self.upload ||
self.sleepAfter && tsNow() > self.sleepAfter
)) {
// console.warn(dT(), 'Send long-poll for DC is delayed', self.dcID, self.sleepAfter);
return;
}
self.sendLongPoll();
});
};
MtpNetworker.prototype.sendLongPoll = function() {
var maxWait = 25000,
self = this;
this.longPollPending = tsNow() + maxWait;
// console.log('Set lp', this.longPollPending, tsNow());
this.wrapMtpCall('http_wait', {
max_delay: 0,
wait_after: 0,
max_wait: maxWait
}, {
noResponse: true,
longPoll: true
}).then(function () {
delete self.longPollPending;
setZeroTimeout(self.checkLongPoll.bind(self));
}, function () {
console.log('Long-poll failed');
});
};
MtpNetworker.prototype.pushMessage = function(message, options) {
var deferred = $q.defer();
this.sentMessages[message.msg_id] = angular.extend(message, options || {}, {deferred: deferred});
this.pendingMessages[message.msg_id] = 0;
if (!options || !options.noShedule) {
this.sheduleRequest();
}
if (angular.isObject(options)) {
options.messageID = message.msg_id;
}
return deferred.promise;
};
MtpNetworker.prototype.pushResend = function(messageID, delay) {
var value = delay ? tsNow() + delay : 0;
var sentMessage = this.sentMessages[messageID];
if (sentMessage.container) {
for (var i = 0; i < sentMessage.inner.length; i++) {
this.pendingMessages[sentMessage.inner[i]] = value;
}
} else {
this.pendingMessages[messageID] = value;
}
// console.log('Resend due', messageID, this.pendingMessages);
this.sheduleRequest(delay);
};
MtpNetworker.prototype.getMsgKeyIv = function (msgKey, isOut) {
var authKey = this.authKeyUint8,
x = isOut ? 0 : 8,
sha1aText = new Uint8Array(48),
sha1bText = new Uint8Array(48),
sha1cText = new Uint8Array(48),
sha1dText = new Uint8Array(48),
promises = {};
sha1aText.set(msgKey, 0);
sha1aText.set(authKey.subarray(x, x + 32), 16);
promises.sha1a = CryptoWorker.sha1Hash(sha1aText);
sha1bText.set(authKey.subarray(x + 32, x + 48), 0);
sha1bText.set(msgKey, 16);
sha1bText.set(authKey.subarray(x + 48, x + 64), 32);
promises.sha1b = CryptoWorker.sha1Hash(sha1bText);
sha1cText.set(authKey.subarray(x + 64, x + 96), 0);
sha1cText.set(msgKey, 32);
promises.sha1c = CryptoWorker.sha1Hash(sha1cText);
sha1dText.set(msgKey, 0);
sha1dText.set(authKey.subarray(x + 96, x + 128), 16);
promises.sha1d = CryptoWorker.sha1Hash(sha1dText);
return $q.all(promises).then(function (result) {
var aesKey = new Uint8Array(32),
aesIv = new Uint8Array(32);
sha1a = new Uint8Array(result.sha1a),
sha1b = new Uint8Array(result.sha1b),
sha1c = new Uint8Array(result.sha1c),
sha1d = new Uint8Array(result.sha1d);
aesKey.set(sha1a.subarray(0, 8));
aesKey.set(sha1b.subarray(8, 20), 8);
aesKey.set(sha1c.subarray(4, 16), 20);
aesIv.set(sha1a.subarray(8, 20));
aesIv.set(sha1b.subarray(0, 8), 12);
aesIv.set(sha1c.subarray(16, 20), 20);
aesIv.set(sha1d.subarray(0, 8), 24);
return [aesKey, aesIv];
});
};
MtpNetworker.prototype.checkConnection = function(event) {
$rootScope.offlineConnecting = true;
console.log(dT(), 'Check connection', event);
$timeout.cancel(this.checkConnectionPromise);
var serializer = new TLSerialization({mtproto: true}),
pingID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];
serializer.storeMethod('ping', {ping_id: pingID});
var pingMessage = {
msg_id: MtpTimeManager.generateID(),
seq_no: this.generateSeqNo(true),
body: serializer.getBytes()
};
var self = this;
this.sendEncryptedRequest(pingMessage, {timeout: 15000}).then(function (result) {
delete $rootScope.offlineConnecting;
self.toggleOffline(false);
}, function () {
console.log(dT(), 'Delay ', self.checkConnectionPeriod * 1000);
self.checkConnectionPromise = $timeout(self.checkConnection.bind(self), parseInt(self.checkConnectionPeriod * 1000));
self.checkConnectionPeriod = Math.min(60, self.checkConnectionPeriod * 1.5);
$timeout(function () {
delete $rootScope.offlineConnecting;
}, 1000);
})
};
MtpNetworker.prototype.toggleOffline = function(enabled) {
// console.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) {
$timeout.cancel(this.nextReqPromise);
delete this.nextReq;
if (this.checkConnectionPeriod < 1.5) {
this.checkConnectionPeriod = 0;
}
this.checkConnectionPromise = $timeout(this.checkConnection.bind(this), parseInt(this.checkConnectionPeriod * 1000));
this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5);
this.onOnlineCb = this.checkConnection.bind(this);
$(document.body).on('online focus', this.onOnlineCb);
} else {
delete this.longPollPending;
this.checkLongPoll();
this.sheduleRequest();
if (this.onOnlineCb) {
$(document.body).off('online focus', this.onOnlineCb);
}
$timeout.cancel(this.checkConnectionPromise);
}
};
MtpNetworker.prototype.performSheduledRequest = function() {
// console.log(dT(), 'sheduled', this.dcID, this.iii);
if (this.offline) {
console.log(dT(), 'Cancel sheduled');
return false;
}
delete this.nextReq;
if (this.pendingAcks.length) {
var ackMsgIDs = [];
for (var i = 0; i < this.pendingAcks.length; i++) {
ackMsgIDs.push(this.pendingAcks[i]);
}
// console.log('acking messages', ackMsgIDs);
this.wrapMtpMessage({_: 'msgs_ack', msg_ids: ackMsgIDs}, {notContentRelated: true, noShedule: true});
}
if (this.pendingResends.length) {
var resendMsgIDs = [],
resendOpts = {noShedule: true, notContentRelated: true};
for (var i = 0; i < this.pendingResends.length; i++) {
resendMsgIDs.push(this.pendingResends[i]);
}
// console.log('resendReq messages', resendMsgIDs);
this.wrapMtpMessage({_: 'msg_resend_req', msg_ids: resendMsgIDs}, resendOpts);
this.lastResendReq = {req_msg_id: resendOpts.messageID, resend_msg_ids: resendMsgIDs};
}
var messages = [],
message,
messagesByteLen = 0,
currentTime = tsNow(),
hasApiCall = false,
hasHttpWait = false,
self = this;
angular.forEach(this.pendingMessages, function (value, messageID) {
if (!value || value >= currentTime) {
if (message = self.sentMessages[messageID]) {
messages.push(message);
messagesByteLen += (message.body.byteLength || message.body.length) + 32;
if (message.isAPI) {
hasApiCall = true;
}
else if (message.longPoll) {
hasHttpWait = true;
}
} else {
// console.log(message, messageID);
}
delete self.pendingMessages[messageID];
}
});
if (hasApiCall && !hasHttpWait) {
var serializer = new TLSerialization({mtproto: true});
serializer.storeMethod('http_wait', {max_delay: 0, wait_after: 0, max_wait: 1000});
messages.push({
msg_id: MtpTimeManager.generateID(),
seq_no: this.generateSeqNo(),
body: serializer.getBytes()
});
}
if (!messages.length) {
// console.log('no sheduled messages');
return;
}
var noResponseMsgs = [];
if (messages.length > 1) {
var container = new TLSerialization({mtproto: true, startMaxLength: messagesByteLen + 64});
container.storeInt(0x73f1f8dc, 'CONTAINER[id]');
container.storeInt(messages.length, 'CONTAINER[count]');
var onloads = [];
var innerMessages = [];
for (var i = 0; i < messages.length; i++) {
container.storeLong(messages[i].msg_id, 'CONTAINER[' + i + '][msg_id]');
innerMessages.push(messages[i].msg_id);
container.storeInt(messages[i].seq_no, 'CONTAINER[' + i + '][seq_no]');
container.storeInt(messages[i].body.length, 'CONTAINER[' + i + '][bytes]');
container.storeRawBytes(messages[i].body, 'CONTAINER[' + i + '][body]');
if (messages[i].noResponse) {
noResponseMsgs.push(messages[i].msg_id);
}
}
var containerSentMessage = {
msg_id: MtpTimeManager.generateID(),
seq_no: this.generateSeqNo(true),
container: true,
inner: innerMessages
}
message = angular.extend({body: container.getBytes(true)}, containerSentMessage);
this.sentMessages[message.msg_id] = containerSentMessage;
if (Config.Modes.debug) {
console.log(dT(), '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 = [];
var self = this;
this.sendEncryptedRequest(message).then(function (result) {
self.toggleOffline(false);
self.parseResponse(result.data).then(function (response) {
if (Config.Modes.debug) {
console.log(dT(), 'Server response', self.dcID, response);
}
self.processMessage(response.response, response.messageID, response.sessionID);
angular.forEach(noResponseMsgs, function (msgID) {
if (self.sentMessages[msgID]) {
var deferred = self.sentMessages[msgID].deferred;
delete self.sentMessages[msgID];
deferred.resolve();
}
});
self.checkLongPoll();
this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod));
});
}, function (error) {
console.log('Encrypted request failed', error);
if (message.container) {
angular.forEach(message.inner, function (msgID) {
self.pendingMessages[msgID] = 0;
});
delete self.sentMessages[message.msg_id];
} else {
self.pendingMessages[message.msg_id] = 0;
}
angular.forEach(noResponseMsgs, function (msgID) {
if (self.sentMessages[msgID]) {
var deferred = self.sentMessages[msgID].deferred;
delete self.sentMessages[msgID];
delete self.pendingMessages[msgID];
deferred.reject();
}
});
self.toggleOffline(true);
});
};
MtpNetworker.prototype.getEncryptedMessage = function (bytes) {
var self = this;
// console.log(dT(), 'Start encrypt', bytes.byteLength);
return CryptoWorker.sha1Hash(bytes).then(function (bytesHash) {
// console.log(dT(), 'after hash');
var msgKey = new Uint8Array(bytesHash).subarray(4, 20);
return self.getMsgKeyIv(msgKey, true).then(function (keyIv) {
// console.log(dT(), 'after msg key iv');
return CryptoWorker.aesEncrypt(bytes, keyIv[0], keyIv[1]).then(function (encryptedBytes) {
// console.log(dT(), 'Finish encrypt');
return {
bytes: encryptedBytes,
msgKey: msgKey
};
})
})
})
};
MtpNetworker.prototype.getDecryptedMessage = function (msgKey, encryptedData) {
// console.log(dT(), 'get decrypted start');
return this.getMsgKeyIv(msgKey, false).then(function (keyIv) {
// console.log(dT(), 'after msg key iv');
return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]);
});
};
MtpNetworker.prototype.sendEncryptedRequest = function (message, options) {
var self = this;
options = options || {};
// console.log(dT(), 'Send encrypted'/*, message*/);
// console.trace();
var data = new TLSerialization({startMaxLength: message.body.length + 64});
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');
return this.getEncryptedMessage(data.getBuffer()).then(function (encryptedResult) {
// console.log(dT(), 'Got encrypted out message'/*, encryptedResult*/);
var request = new TLSerialization({startMaxLength: encryptedResult.bytes.byteLength + 256});
request.storeIntBytes(self.authKeyID, 64, 'auth_key_id');
request.storeIntBytes(encryptedResult.msgKey, 128, 'msg_key');
request.storeRawBytes(encryptedResult.bytes, 'encrypted_data');
var requestData = xhrSendBuffer ? request.getBuffer() : request.getArray();
var requestPromise;
try {
options = angular.extend(options || {}, {
responseType: 'arraybuffer',
transformRequest: null
});
requestPromise = $http.post(MtpDcConfigurator.chooseServer(self.dcID) + '/apiw1', requestData, options);
} catch (e) {
requestPromise = $q.reject(e);
}
return requestPromise.then(
function (result) {
if (!result.data || !result.data.byteLength) {
return $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE'});
}
return result;
},
function (error) {
if (error.status == 404 &&
(error.data || '').indexOf('nginx/0.3.33') != -1) {
Storage.remove(
'dc' + self.dcID + '_server_salt',
'dc' + self.dcID + '_auth_key'
).then(function () {
AppRuntimeManager.reload();
});
}
if (!error.message && !error.type) {
error = {code: 406, type: 'NETWORK_BAD_REQUEST'};
}
return $q.reject(error);
}
);
});
};
MtpNetworker.prototype.parseResponse = function (responseBuffer) {
// console.log(dT(), 'Start parsing response');
var self = this;
var deserializer = new TLDeserialization(responseBuffer);
var authKeyID = deserializer.fetchIntBytes(64, false, 'auth_key_id');
if (!bytesCmp(authKeyID, this.authKeyID)) {
throw new Error('Invalid server auth_key_id: ' + bytesToHex(authKeyID));
}
var msgKey = deserializer.fetchIntBytes(128, true, 'msg_key'),
encryptedData = deserializer.fetchRawBytes(responseBuffer.byteLength - deserializer.getOffset(), true, 'encrypted_data');
return this.getDecryptedMessage(msgKey, encryptedData).then(function (dataWithPadding) {
// console.log(dT(), 'after decrypt');
var deserializer = new TLDeserialization(dataWithPadding, {mtproto: true});
var salt = deserializer.fetchIntBytes(64, false, 'salt');
var sessionID = deserializer.fetchIntBytes(64, false, 'session_id');
var messageID = deserializer.fetchLong('message_id');
var seqNo = deserializer.fetchInt('seq_no');
var messageBody = deserializer.fetchRawBytes(false, true, 'message_data');
// console.log(dT(), 'before hash');
var hashData = convertToUint8Array(dataWithPadding).subarray(0, deserializer.getOffset());
return CryptoWorker.sha1Hash(hashData).then(function (dataHash) {
if (!bytesCmp(msgKey, bytesFromArrayBuffer(dataHash).slice(-16))) {
console.warn(msgKey, bytesFromArrayBuffer(dataHash));
throw new Error('server msgKey mismatch');
}
// console.log(dT(), 'after hash check');
var buffer = bytesToArrayBuffer(messageBody);
var deserializerOptions = {
mtproto: true,
override: {
mt_message: function (result, field) {
result.msg_id = this.fetchLong(field + '[msg_id]');
result.seqno = this.fetchInt(field + '[seqno]');
result.bytes = this.fetchInt(field + '[bytes]');
var offset = this.getOffset();
try {
result.body = this.fetchObject('Object', field + '[body]');
} catch (e) {
console.error(dT(), 'parse error', e.message, e.stack);
result.body = {_: 'parse_error', error: e};
}
if (this.offset != offset + result.bytes) {
console.warn(dT(), 'set offset', this.offset, offset, result.bytes);
// console.log(dT(), result);
this.offset = offset + result.bytes;
}
// console.log(dT(), 'override message', result);
},
mt_rpc_result: function (result, field) {
result.req_msg_id = this.fetchLong(field + '[req_msg_id]');
var sentMessage = self.sentMessages[result.req_msg_id],
type = sentMessage && sentMessage.resultType || 'Object';
result.result = this.fetchObject(type, field + '[result]');
// console.log(dT(), 'override rpc_result', type, result);
}
}
};
var deserializer = new TLDeserialization(buffer, deserializerOptions);
var response = deserializer.fetchObject('', 'INPUT');
// console.log(dT(), 'after fetch');
return {
response: response,
messageID: messageID,
sessionID: sessionID,
seqNo: seqNo
};
});
});
};
MtpNetworker.prototype.applyServerSalt = function (newServerSalt) {
var serverSalt = longToBytes(newServerSalt);
var storeObj = {};
storeObj['dc' + this.dcID + '_server_salt'] = bytesToHex(serverSalt);
Storage.set(storeObj);
this.serverSalt = serverSalt;
return true;
};
MtpNetworker.prototype.sheduleRequest = function (delay) {
if (this.offline) {
this.checkConnection('forced shedule');
}
var nextReq = tsNow() + delay;
if (delay && this.nextReq && this.nextReq <= nextReq) {
return false;
}
// console.log(dT(), 'shedule req', delay);
// console.trace();
$timeout.cancel(this.nextReqPromise);
if (delay > 0) {
this.nextReqPromise = $timeout(this.performSheduledRequest.bind(this), delay || 0);
} else {
setZeroTimeout(this.performSheduledRequest.bind(this))
}
this.nextReq = nextReq;
};
MtpNetworker.prototype.onSessionCreate = function (sessionID, messageID) {
// console.log(dT(), 'New session created', bytesToHex(sessionID));
};
MtpNetworker.prototype.ackMessage = function (msgID) {
// console.log('ack message', msgID);
this.pendingAcks.push(msgID);
this.sheduleRequest(30000);
};
MtpNetworker.prototype.reqResendMessage = function (msgID) {
console.log(dT(), 'Req resend', msgID);
this.pendingResends.push(msgID);
this.sheduleRequest(100);
};
MtpNetworker.prototype.cleanupSent = function () {
var self = this;
var notEmpty = false;
// console.log('clean start', this.dcID/*, this.sentMessages*/);
angular.forEach(this.sentMessages, function(message, msgID) {
// console.log('clean iter', msgID, message);
if (message.notContentRelated && self.pendingMessages[msgID] === undefined) {
// console.log('clean notContentRelated', msgID);
delete self.sentMessages[msgID];
}
else if (message.container) {
for (var i = 0; i < message.inner.length; i++) {
if (self.sentMessages[message.inner[i]] !== undefined) {
// console.log('clean failed, found', msgID, message.inner[i], self.sentMessages[message.inner[i]].seq_no);
notEmpty = true;
return;
}
}
// console.log('clean container', msgID);
delete self.sentMessages[msgID];
} else {
notEmpty = true;
}
});
return !notEmpty;
};
MtpNetworker.prototype.processMessageAck = function (messageID) {
var sentMessage = this.sentMessages[messageID];
if (sentMessage && !sentMessage.acked) {
delete sentMessage.body;
sentMessage.acked = true;
return true;
}
return false;
};
MtpNetworker.prototype.processError = function (rawError) {
var matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || [];
rawError.error_code = uintToInt(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
};
};
MtpNetworker.prototype.processMessage = function (message, messageID, sessionID) {
// console.log('process message', message, messageID, sessionID);
switch (message._) {
case 'msg_container':
var len = message.messages.length;
for (var i = 0; i < len; i++) {
this.processMessage(message.messages[i], messageID, sessionID);
}
break;
case 'bad_server_salt':
console.log(dT(), 'Bad server salt', message);
var sentMessage = this.sentMessages[message.bad_msg_id];
if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
console.log(message.bad_msg_id, message.bad_msg_seqno);
throw new Error('Bad server salt for invalid message');
}
this.applyServerSalt(message.new_server_salt);
this.pushResend(message.bad_msg_id);
this.ackMessage(messageID);
break;
case 'bad_msg_notification':
console.log(dT(), 'Bad msg notification', message);
var sentMessage = this.sentMessages[message.bad_msg_id];
if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
console.log(message.bad_msg_id, message.bad_msg_seqno);
throw new Error('Bad msg notification for invalid message');
}
if (message.error_code == 16 || message.error_code == 17) {
if (MtpTimeManager.applyServerTime(
bigStringInt(messageID).shiftRight(32).toString(10)
)) {
console.log(dT(), 'Update session');
this.updateSession();
}
var badMessage = this.updateSentMessage(message.bad_msg_id);
this.pushResend(badMessage.msg_id);
this.ackMessage(messageID);
}
break;
case 'message':
this.serverMessages.push(message.msg_id);
this.processMessage(message.body, message.msg_id, sessionID);
break;
case 'new_session_created':
this.ackMessage(messageID);
this.processMessageAck(message.first_msg_id);
this.applyServerSalt(message.server_salt);
this.onSessionCreate(sessionID, messageID);
break;
case 'msgs_ack':
for (var i = 0; i < message.msg_ids.length; i++) {
this.processMessageAck(message.msg_ids[i]);
}
break;
case 'msg_detailed_info':
if (!this.sentMessages[message.msg_id]) {
this.ackMessage(message.answer_msg_id);
break;
}
case 'msg_new_detailed_info':
// this.ackMessage(message.answer_msg_id);
this.reqResendMessage(message.answer_msg_id);
break;
case 'msgs_state_info':
this.ackMessage(message.answer_msg_id);
if (this.lastResendReq && this.lastResendReq.req_msg_id == message.req_msg_id && this.pendingResends.length) {
var i, badMsgID, pos;
for (i = 0; i < this.lastResendReq.resend_msg_ids.length; i++) {
badMsgID = this.lastResendReq.resend_msg_ids[i];
pos = this.pendingResends.indexOf(badMsgID);
if (pos != -1) {
this.pendingResends.splice(pos, 1);
}
}
}
break;
case 'rpc_result':
this.ackMessage(messageID);
var sentMessageID = message.req_msg_id,
sentMessage = this.sentMessages[sentMessageID];
this.processMessageAck(sentMessageID);
if (sentMessage) {
var deferred = sentMessage.deferred;
if (message.result._ == 'rpc_error') {
var error = this.processError(message.result);
console.log(dT(), 'Rpc error', error)
if (deferred) {
deferred.reject(error)
}
} else {
if (deferred) {
if (Config.Modes.debug) {
console.log(dT(), 'Rpc response', message.result);
} else {
var dRes = message.result._;
if (!dRes) {
if (message.result.length > 5) {
dRes = '[..' + message.result.length + '..]';
} else {
dRes = message.result;
}
}
console.log(dT(), 'Rpc response', dRes);
}
sentMessage.deferred.resolve(message.result);
}
if (sentMessage.isAPI) {
this.connectionInited = true;
}
}
delete this.sentMessages[sentMessageID];
}
break;
default:
this.ackMessage(messageID);
// console.log('Update', message);
if (updatesProcessor) {
updatesProcessor(message);
}
break;
}
};
return {
getNetworker: function (dcID, authKey, serverSalt, options) {
return new MtpNetworker(dcID, authKey, serverSalt, options);
},
setUpdatesProcessor: function (callback) {
updatesProcessor = callback;
}
};
})