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.
1584 lines
50 KiB
1584 lines
50 KiB
/*! |
|
* Webogram v0.4.1 - 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 sslSubdomains = ['pluto', 'venus', 'aurora', 'vesta', 'flora']; |
|
|
|
var dcOptions = Config.Modes.test |
|
? [ |
|
{id: 1, host: '149.154.175.10', port: 80}, |
|
{id: 2, host: '149.154.167.40', port: 80}, |
|
{id: 3, host: '174.140.142.5', port: 80} |
|
] |
|
: [ |
|
{id: 1, host: '149.154.175.50', port: 80}, |
|
{id: 2, host: '149.154.167.51', port: 80}, |
|
{id: 3, host: '149.154.175.100', port: 80}, |
|
{id: 4, host: '149.154.167.91', port: 80}, |
|
{id: 5, host: '149.154.171.5', port: 80} |
|
]; |
|
|
|
var chosenServers = {}; |
|
|
|
function chooseServer(dcID, upload) { |
|
if (chosenServers[dcID] === undefined) { |
|
var chosenServer = false, |
|
i, dcOption; |
|
|
|
if (Config.Modes.ssl) { |
|
var subdomain = sslSubdomains[dcID - 1] + (upload ? '-1' : ''); |
|
var path = Config.Modes.test ? 'apiw_test1' : 'apiw1'; |
|
chosenServer = 'https://' + subdomain + '.web.telegram.org/' + path; |
|
return chosenServer; |
|
} |
|
|
|
for (i = 0; i < dcOptions.length; i++) { |
|
dcOption = dcOptions[i]; |
|
if (dcOption.id == dcID) { |
|
chosenServer = 'http://' + dcOption.host + (dcOption.port != 80 ? ':' + dcOption.port : '') + '/apiw1'; |
|
break; |
|
} |
|
} |
|
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 ($window) { |
|
$($window).on('click keydown', rng_seed_time); |
|
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, timeOffset); |
|
|
|
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), 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}); |
|
} |
|
|
|
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, |
|
akStopped = 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(0xda9b0d0d, 'invokeWithLayer'); |
|
serializer.storeInt(Config.Schema.API.layer, 'layer'); |
|
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 || |
|
akStopped) { |
|
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 || akStopped) { |
|
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, |
|
lengthOverflow = false, |
|
singlesCount = 0, |
|
self = this; |
|
|
|
angular.forEach(this.pendingMessages, function (value, messageID) { |
|
if (!value || value >= currentTime) { |
|
if (message = self.sentMessages[messageID]) { |
|
var messageByteLength = (message.body.byteLength || message.body.length) + 32; |
|
if (!message.notContentRelated && |
|
lengthOverflow) { |
|
return; |
|
} |
|
if (!message.notContentRelated && |
|
messagesByteLen && |
|
messagesByteLen + messageByteLength > 655360) { // 640 Kb |
|
lengthOverflow = true; |
|
return; |
|
} |
|
if (message.singleInRequest) { |
|
singlesCount++; |
|
if (singlesCount > 1) { |
|
return; |
|
} |
|
} |
|
messages.push(message); |
|
messagesByteLen += messageByteLength; |
|
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); |
|
}); |
|
|
|
if (lengthOverflow || singlesCount > 1) { |
|
this.sheduleRequest() |
|
} |
|
}; |
|
|
|
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, self.upload), 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'); |
|
} |
|
|
|
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'); |
|
|
|
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.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); |
|
|
|
var self = this; |
|
Storage.get('dc').then(function (baseDcID) { |
|
if (baseDcID == self.dcID && !self.upload && updatesProcessor) { |
|
updatesProcessor(message); |
|
} |
|
}); |
|
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; |
|
|
|
} |
|
}; |
|
|
|
function startAll() { |
|
if (akStopped) { |
|
akStopped = false; |
|
updatesProcessor({_: 'new_session_created'}); |
|
} |
|
} |
|
|
|
function stopAll() { |
|
akStopped = true; |
|
} |
|
|
|
return { |
|
getNetworker: function (dcID, authKey, serverSalt, options) { |
|
return new MtpNetworker(dcID, authKey, serverSalt, options); |
|
}, |
|
setUpdatesProcessor: function (callback) { |
|
updatesProcessor = callback; |
|
}, |
|
stopAll: stopAll, |
|
startAll: startAll |
|
}; |
|
|
|
})
|
|
|