UNPKG

madeline-ton

Version:

Pure JS client-side implementation of the Telegram TON blockchain protocol

933 lines (782 loc) 32.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _network = _interopRequireDefault(require("./network")); var _stream = _interopRequireDefault(require("./TL/stream")); var _messageIdHandler = _interopRequireDefault(require("./session/messageIdHandler")); var _http = _interopRequireDefault(require("./network/http")); var _tools = require("./tools"); var _random = require("./crypto-sync/random"); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } var NOT_CONTENT_RELATED = [//'rpc_result', //'rpc_error', 'rpc_drop_answer', 'rpc_answer_unknown', 'rpc_answer_dropped_running', 'rpc_answer_dropped', 'get_future_salts', 'future_salt', 'future_salts', 'ping', 'pong', 'ping_delay_disconnect', 'destroy_session', 'destroy_session_ok', 'destroy_session_none', //'new_session_created', 'msg_container', 'msg_copy', 'gzip_packed', 'http_wait', 'msgs_ack', 'bad_msg_notification', 'bad_server_salt', 'msgs_state_req', 'msgs_state_info', 'msgs_all_info', 'msg_detailed_info', 'msg_new_detailed_info', 'msg_resend_req', 'msg_resend_ans_req']; var Connection = /*#__PURE__*/ function () { (0, _createClass2["default"])(Connection, [{ key: "resetSession", /** * Reset MTProto session */ value: function () { var _resetSession = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee() { return _regenerator["default"].wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return this.crypto.secureRandom(new Uint32Array(2)); case 2: this.sessionId = _context.sent; this.inSeqNo = 0; this.outSeqNo = 0; this.mIdHandler = new _messageIdHandler["default"](); case 6: case "end": return _context.stop(); } } }, _callee, this); })); function resetSession() { return _resetSession.apply(this, arguments); } return resetSession; }() /** * Create MTProto session if needed */ }, { key: "createSession", value: function () { var _createSession = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee2() { return _regenerator["default"].wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: if (!(typeof this.sessionId === 'undefined')) { _context2.next = 6; break; } _context2.next = 3; return this.crypto.secureRandom(new Uint32Array(2)); case 3: this.sessionId = _context2.sent; this.inSeqNo = 0; this.outSeqNo = 0; case 6: case "end": return _context2.stop(); } } }, _callee2, this); })); function createSession() { return _createSession.apply(this, arguments); } return createSession; }() /** * Check if is content related * @param {string} constructor * @boolean True if content related */ }, { key: "contentRelated", value: function contentRelated(constructor) { return !NOT_CONTENT_RELATED.includes(constructor); } /** * * @param {boolean} contentRelated Whether is content related * @returns {number} Seqno */ }, { key: "generateOutSeqNo", value: function generateOutSeqNo(contentRelated) { var value = this.outSeqNo; this.outSeqNo += contentRelated; return value * 2 + contentRelated; } /** * * @param {AuthInfo} shared * @param {API} API */ }]); function Connection(authInfo, API) { (0, _classCallCheck2["default"])(this, Connection); (0, _defineProperty2["default"])(this, "incomingMessages", {}); (0, _defineProperty2["default"])(this, "outgoingMessages", {}); (0, _defineProperty2["default"])(this, "newOutgoing", {}); (0, _defineProperty2["default"])(this, "newIncoming", {}); (0, _defineProperty2["default"])(this, "pendingOutgoingMessages", {}); (0, _defineProperty2["default"])(this, "pendingOutgoingKey", 0); (0, _defineProperty2["default"])(this, "toAck", []); (0, _defineProperty2["default"])(this, "connected", false); (0, _defineProperty2["default"])(this, "inSeqNo", 0); (0, _defineProperty2["default"])(this, "outSeqNo", 0); this.authInfo = authInfo; this.API = API; this.TL = API.getTL(); this.mIdHandler = new _messageIdHandler["default"](); } /** * Connect to datacenter * @param {Context} ctx */ (0, _createClass2["default"])(Connection, [{ key: "connect", value: function connect(ctx) { var _this = this; this.dc = ctx.getDcId(); this.ctx = ctx; this.crypto = ctx.getCrypto(); console.log("Connecting to DC ".concat(this.dc, "...")); this.connection = new _network["default"](); this.connection.onMessage = this.onMessage.bind(this); this.connection.onClose = function () { return _this.connected = false; }; return this.connection.connect(ctx).then(function () { return _this.connected = true; }); } }, { key: "getBuffer", value: function getBuffer() { return this.authInfo.hasAuthKey() ? new _stream["default"]() : this.connection.getBuffer(); } /** * * [ * // only in outgoing messages * 'body' => deserialized body, (optional if container) * 'serializedBody' => 'serialized body', (optional if container) * 'contentRelated' => bool, * '_' => 'predicate', * 'promise' => deferred promise that gets resolved when a response to the message is received (optional), * 'send_promise' => deferred promise that gets resolved when the message is sent (optional), * 'file' => bool (optional), * 'type' => 'type' (optional), * 'queue' => queue ID (optional), * 'container' => [message ids] (optional), * * // only in incoming messages * 'content' => deserialized body, * 'seq_no' => number (optional), * 'from_container' => bool (optional), * * // can be present in both * 'response' => message id (optional), * 'msg_id' => message id (optional), * 'sent' => timestamp, * 'tries' => number * ] * * @param {Message} message Message object * @param boolean flush Whether to flush */ }, { key: "sendMessage", value: function sendMessage(message) { var flush = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; if (!message['serializedBody']) { if (message['unencrypted']) { message['type'] = this.TL.objects.findByPredicateAndLayer(message['_'])['type']; } var stream = this.getBuffer(); this.TL.serialize(stream, message['body'], { layer: this.API.layer }); stream.pos = stream.initPos + 4; stream.writeUnsignedInt(stream.getByteLength() - (5 + stream.initPos) * 4); stream.pos = stream.initPos; message['serializedBody'] = stream; } this.pendingOutgoingMessages[this.pendingOutgoingKey++] = message; //this.pendingOutgoingKey %= 0xFFFFFFFF if (flush) { return this.flush(); } } /** * * @param {string} method Method name * @param {Object} args Arguments * @param {Object} aargs Additional arguments */ }, { key: "methodCall", value: function () { var _methodCall = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee3(method, args, aargs) { var message, promise; return _regenerator["default"].wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: args['_'] = method; message = _objectSpread({ _: method, method: true, body: args, unencrypted: !this.authInfo.hasAuthKey() && !method.includes('.'), contentRelated: this.contentRelated(method) }, aargs); promise = new Promise(function (res, rej) { message['resolve'] = res, message['reject'] = rej; }); _context3.next = 5; return this.sendMessage(message, true); case 5: return _context3.abrupt("return", promise); case 6: case "end": return _context3.stop(); } } }, _callee3, this); })); function methodCall(_x, _x2, _x3) { return _methodCall.apply(this, arguments); } return methodCall; }() /** * Send pending outgoing messages */ }, { key: "flush", value: function () { var _flush = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee4() { return _regenerator["default"].wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: if (!this.authInfo.hasAuthKey()) { _context4.next = 5; break; } _context4.next = 3; return this.flushEncrypted(); case 3: _context4.next = 7; break; case 5: _context4.next = 7; return this.flushPlain(); case 7: case "end": return _context4.stop(); } } }, _callee4, this); })); function flush() { return _flush.apply(this, arguments); } return flush; }() }, { key: "flushPlain", value: function () { var _flushPlain = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee5() { var pendingMessages, key, message, messageId; return _regenerator["default"].wrap(function _callee5$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: if (Object.keys(this.pendingOutgoingMessages).length) { _context5.next = 2; break; } return _context5.abrupt("return"); case 2: pendingMessages = this.pendingOutgoingMessages; this.pendingOutgoingMessages = {}; this.pendingOutgoingKey = 0; _context5.t0 = _regenerator["default"].keys(pendingMessages); case 6: if ((_context5.t1 = _context5.t0()).done) { _context5.next = 26; break; } key = _context5.t1.value; message = pendingMessages[key]; if (!this.authInfo.hasAuthKey()) { _context5.next = 11; break; } return _context5.abrupt("return"); case 11: if (message['unencrypted']) { _context5.next = 13; break; } return _context5.abrupt("continue", 6); case 13: messageId = message['msg_id'] || this.mIdHandler.generate(); message['serializedBody'].pos = message['serializedBody'].initPos + 2; message['serializedBody'].writeSignedLong([messageId.low_, messageId.high_]); message['serializedBody'].pos = message['serializedBody'].initPos; _context5.next = 19; return this.connection.write(message['serializedBody']); case 19: message['sent'] = Date.now() / 1000; message['tries'] = 0; this.outgoingMessages[messageId.toString()] = message; this.newOutgoing[messageId.toString()] = messageId; console.log("Sent ".concat(message['_'], " as unencrypted message to DC ").concat(this.dc, "!")); _context5.next = 6; break; case 26: case "end": return _context5.stop(); } } }, _callee5, this); })); function flushPlain() { return _flushPlain.apply(this, arguments); } return flushPlain; }() }, { key: "flushEncrypted", value: function () { var _flushEncrypted = (0, _asyncToGenerator2["default"])( /*#__PURE__*/ _regenerator["default"].mark(function _callee6() { var skipped, hasVal, temporaryKeys, x, hasHttpWait, k, keys, totalLength, count, inited, messages, _k, message, actualLength, _messageId, MTmessage, messageData, messageId, seqNo, _message, length, plainLength, padding, buffer, authKey, sha, messageKey, pair, cipher, sent, _k2, mId; return _regenerator["default"].wrap(function _callee6$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: skipped = false; hasVal = false; case 2: if (this.authInfo.hasAuthKey()) { _context6.next = 4; break; } return _context6.abrupt("return"); case 4: if (!(this.connection instanceof _http["default"] && !Object.keys(this.pendingOutgoingMessages).length)) { _context6.next = 6; break; } return _context6.abrupt("return"); case 6: temporaryKeys = []; if (this.toAck.length) { for (x = 0; x < this.toAck.length; x += 8192) { console.log("Adding msgs_ack ".concat(this.pendingOutgoingKey)); this.pendingOutgoingMessages[this.pendingOutgoingKey] = { contentRelated: false, unencrypted: false, method: false, serializedBody: this.TL.serialize(new _stream["default"](), { _: msgs_ack, msg_ids: this.toAck.slice(x, x + 8192) }) }; temporaryKeys.push(this.pendingOutgoingKey); this.pendingOutgoingKey++; } } hasHttpWait = false; if (this.connection instanceof _http["default"]) { for (k in this.pendingOutgoingMessages) { if (this.pendingOutgoingMessages[k]['_'] === 'http_wait') { hasHttpWait = true; } } if (!hasHttpWait) { console.log("Adding http_wait ".concat(this.pendingOutgoingKey)); this.pendingOutgoingMessages[this.pendingOutgoingKey] = { _: 'http_wait', serializedBody: this.TL.serialize(new _stream["default"](), { _: http_wait, max_wait: 30000, wait_after: 0, max_delay: 0 }), contentRelated: true, unencrypted: false, method: true }; temporaryKeys.push(this.pendingOutgoingKey); this.pendingOutgoingKey++; temporaryKeys.push(this.pendingOutgoingKey); } } keys = []; totalLength = 0; count = 0; inited = false; messages = []; _context6.t0 = _regenerator["default"].keys(this.pendingOutgoingMessages); case 16: if ((_context6.t1 = _context6.t0()).done) { _context6.next = 43; break; } _k = _context6.t1.value; message = this.pendingOutgoingMessages[_k]; if (!message['unencrypted']) { _context6.next = 21; break; } return _context6.abrupt("continue", 16); case 21: if (!message['container']) { _context6.next = 24; break; } delete this.pendingOutgoingMessages[_k]; return _context6.abrupt("continue", 16); case 24: // This isn't used for now /* if (this.API.settings.pfs && !this.authInfo.bound() && message['method'] && !['http_wait', 'auth.bindTempAuthKey'].contains(message['_'])) { console.log(`Skipping {$message['_']} due to unbound keys in DC {$datacenter}`); skipped = true; continue; }*/ actualLength = message['serializedBody'].getByteLength() + 32; if (!(totalLength && totalLength + actualLength > 32760 || count >= 1020)) { _context6.next = 28; break; } console.log('Length overflow, postponing part of payload'); return _context6.abrupt("break", 43); case 28: _messageId = message['messageId'] || this.mIdHandler.generate(); console.log("Sending ".concat(message['_'], " as encrypted message to DC ").concat(this.dc)); MTmessage = { _: 'message', // layer 1 msg_id: _messageId, body: message['serializedBody'].bBuf, seqno: this.generateOutSeqNo(message['contentRelated']) }; if (message['method'] && message['_'] !== 'http_wait') { if (!this.authInfo.getAuthKey(true).isInited() && message['_'] !== 'auth.bindTempAuthKey' && !inited) { inited = true; console.log("Writing client info by wrapping ".concat(message['_'])); MTmessage['body'] = this.TL.serialize(new _stream["default"](), { _: 'invokeWithLayer', layer: this.API.layer, query: this.TL.serialize(new _stream["default"](), { _: 'initConnection', api_id: 683677, api_hash: '6e1ce1db80b068b718fe39fd0523d433', device_model: navigator.userAgent || 'Unknown UserAgent', system_version: navigator.platform || 'Unknown Platform', app_version: 1, system_lang_code: navigator.language || 'en', lang_pack: '', lang_code: navigator.language || 'en', query: MTmessage['body'] }).bBuf }).bBuf; } } actualLength = MTmessage['body'].byteLength + 32; if (!(totalLength && totalLength + actualLength > 32760)) { _context6.next = 36; break; } console.log('Length overflow, postponing part of payload'); return _context6.abrupt("break", 43); case 36: count++; totalLength += actualLength; MTmessage['bytes'] = MTmessage['body'].byteLength; messages.push(MTmessage); keys[_k] = _messageId; _context6.next = 16; break; case 43: messageData = void 0, messageId = void 0, seqNo = void 0; if (!(count > 1)) { _context6.next = 53; break; } console.log("Wrapping in msg_container ".concat(count, " message of total length ").concat(totalLength, " as encrypted message for DC ").concat(this.dc)); messageId = this.mIdHandler.generate(); this.pendingOutgoingMessages[this.pendingOutgoingKey] = { _: 'msg_container', container: keys, contentRelated: false, method: false, unencrypted: false }; keys[this.pendingOutgoingKey++] = messageId; messageData = this.TL.serialize(new _stream["default"](), { _: 'msg_container', messages: messages }, { layer: 1 }).bBuf; seqNo = this.generateOutSeqNo(false); _context6.next = 62; break; case 53: if (!count) { _context6.next = 60; break; } _message = messages[0]; messageData = _message['body']; messageId = _message['msg_id']; seqNo = _message['seqno']; _context6.next = 62; break; case 60: console.warn("No message was sent in DC ".concat(this.dc)); return _context6.abrupt("return"); case 62: messages = undefined; length = messageData.byteLength; plainLength = 32 + length; padding = (0, _tools.posMod)(-plainLength, 16); if (padding < 12) { padding += 16; } buffer = new _stream["default"](new Uint8Array(plainLength + padding).buffer); buffer.writeUnsignedInts(this.authInfo.getAuthKey().getServerSalt()); buffer.writeUnsignedInts(this.sessionId); buffer.writeSignedLong([messageId.low_, messageId.high_]); buffer.writeUnsignedInt(seqNo); buffer.writeUnsignedInt(length); buffer.bBuf.set(messageData, 32); (0, _random.fastRandom)(buffer.bBuf.subarray(plainLength)); authKey = this.authInfo.getAuthKey().getAuthKey(); console.log(buffer.bBuf); _context6.next = 79; return this.crypto.sha256((0, _tools.bufferConcat)(authKey.subarray(88, 120), buffer.bBuf)); case 79: sha = _context6.sent; messageKey = new Uint8Array(sha).slice(8, 24); _context6.next = 83; return this.crypto.aesCalculate(messageKey, authKey); case 83: pair = _context6.sent; cipher = this.connection.getBuffer(1 + buffer.uBuf.length); cipher.pos = cipher.initPos; cipher.writeUnsignedInts(this.authInfo.getAuthKey().getID()); cipher.writeUnsignedInts(new Uint32Array(messageKey.buffer)); _context6.t2 = cipher; _context6.t3 = Uint32Array; _context6.next = 92; return this.crypto.igeEncrypt(buffer.uBuf, pair[0], pair[1]); case 92: _context6.t4 = _context6.sent; _context6.t5 = new _context6.t3(_context6.t4); _context6.t2.writeUnsignedInts.call(_context6.t2, _context6.t5); this.connection.write(cipher); sent = Math.floor(Date.now() / 1000); if (this.toAck.length) { this.toAck = []; } for (_k2 in keys) { mId = keys[_k2]; this.outgoingMessages[mId] = this.pendingOutgoingMessages[_k2]; if (this.outgoingMessages[mId]['resolve']) { this.outgoingMessages[mId]['sent'] = sent; this.outgoingMessages[mId]['tries'] = 0; } delete this.pendingOutgoingMessages[_k2]; } console.log("Sent encrypted payload to DC ".concat(this.dc)); case 100: if ((hasVal = Object.keys(this.pendingOutgoingMessages).length) && !skipped) { _context6.next = 2; break; } case 101: if (!hasVal) { this.pendingOutgoingKey = 0; } case 102: case "end": return _context6.stop(); } } }, _callee6, this); })); function flushEncrypted() { return _flushEncrypted.apply(this, arguments); } return flushEncrypted; }() /** * Parse incoming message * @param {ArrayBuffer} message */ }, { key: "onMessage", value: function onMessage(message) { console.log("Got message"); //console.log(message) //console.log(message.getByteLength()) if (message.getByteLength() === 4) { var error = message.readSignedInt(); throw new Error(error); if (error === -404) { if (this.authInfo.hasAuthKey()) { console.log("Resetting auth key in DC " + this.dc); this.authInfo.setAuthKey(undefined); this.resetSession(); } } } var authKey = message.readSignedLong(); var mId; if (authKey[0] + authKey[1] === 0) { mId = this.mIdHandler.check(message.readSignedLong()); message.pos++; // Skip length, framing is handled correctly by bodelaysth HTTP and websockets anyway } else {} this.incomingMessages[mId] = this.TL.deserialize(message); this.newIncoming[mId] = mId; this.handleMessages(); // It might be worth moving the entire Connection module into a worker to avoid main thread delays due to GZIP decoding. // Could implement later. // Could also make the deserialize function an async function, but I really don't like to have regenerator runtime for such a vital and heavily used function. /* this.crypto.deserialize(message).then(message => { this.incomingMessages[mId] = message this.newIncoming[mId] = mId this.handleMessages() })*/ } }, { key: "handleMessages", value: function handleMessages() { while (Object.keys(this.newIncoming).length) { var message, mId; for (var key in this.newIncoming) { // Avoid problems with multiple competing calls mId = key; message = this.incomingMessages[key]; delete this.incomingMessages[key]; delete this.newIncoming[key]; break; } console.log("Received ".concat(message['_'], " from DC ").concat(this.dc)); switch (message['_']) { case 'msgs_ack': console.log("msgs_ack: ".concat(message)); break; case 'rpc_result': this.toAck.push(mId); this.handleResponse(mId, message); break; case 'future_salts': case 'msgs_state_info': var msg_id_type = 'req_msg_id'; // no break case 'bad_server_salt': case 'bad_msg_notification': var msg_id_type = msg_id_type ? msg_id_type : 'bad_msg_id'; // no break case 'pong': var msg_id_type = msg_id_type ? msg_id_type : 'msg_id'; this.handleResponse(message[msg_id_type], message); break; case 'new_session_created': console.log("new_session_created: ".concat(message)); break; case 'msg_container': for (mId in message['messages']) { this.mIdHandler.check(mId, true); this.incomingMessages[mId] = message['messages'][mId]; this.newIncoming[mId] = mId; } break; default: var type = this.TL.objects.findByPredicateAndLayer(message['_'])['type']; if (type === 'Updates') { // Do update handling stuffs break; } //console.log(`Trying to assing a raw response of type ${type} to its request...`) for (var rId in this.newOutgoing) { //console.log(`Does the request of return type ${this.outgoingMessages[rId]['type']} match?`) if (this.outgoingMessages[rId]['type'] === type) { //console.log('Yes') this.handleResponse(rId, message); break; } //console.log('No') } } } } }, { key: "handleResponse", value: function handleResponse(reqId, response) { var request = this.outgoingMessages[reqId]; if (typeof request === 'undefined') { console.log("Could not find request for reqId ".concat(reqId)); return; } if ((0, _typeof2["default"])(response['_']) !== undefined) { // Might be a vector return type switch (response['_']) { case 'rpc_error': } } if (request['method'] && request['_'] === 'auth.bindTempAuthKey' && this.authInfo.hasAuthKey() && !this.authInfo.getAuthKey().isInited()) { this.authInfo.getAuthKey().init(true); } if (!request['resolve']) { console.log("Already resolved ".concat(response['_'] ? response['_'] : '-')); return; } if (response['_'] && this.TL.objects.findByPredicateAndLayer(request['_'])['type'] === 'Updates') { response['request'] = request; // For better peer resolution in results of method calls // Here should call update handler } this.gotResponseForOutgoingMessageId(reqId); var r = request['resolve']; delete request['reject']; delete request['resolve']; r(response); } }, { key: "gotResponseForOutgoingMessageId", value: function gotResponseForOutgoingMessageId(id) { if (this.newOutgoing[id]) { delete this.newOutgoing[id]; } if (this.outgoingMessages[id]['body']) { delete this.outgoingMessages[id]['body']; } if (this.outgoingMessages[id]['serializedBody']) { delete this.outgoingMessages[id]['serializedBody']; } } }]); return Connection; }(); var _default = Connection; exports["default"] = _default;