├── .eslintignore ├── index.js ├── config ├── index.js └── webogram │ ├── webpack.vendor.config.js │ └── webpack.config.js ├── src ├── util │ ├── dtime.js │ ├── switch.js │ ├── defer.js │ ├── smart-timeout.js │ └── log.js ├── http.js ├── service │ ├── updates.h.js │ ├── secure-random.js │ ├── main │ │ ├── wrap.js │ │ ├── invoke-layer-generator.js │ │ ├── index.h.js │ │ ├── config-validation.js │ │ └── index.js │ ├── networker │ │ ├── net-message.js │ │ └── state.js │ ├── api-manager │ │ ├── index.h.js │ │ ├── request.js │ │ ├── error-cases.js │ │ └── index.js │ ├── time-manager.js │ ├── rsa-keys-manger.js │ ├── dc-configurator.js │ ├── authorizer │ │ ├── send-plain-req.js │ │ └── index.js │ └── updates.js ├── index.js ├── layout │ ├── index.h.js │ └── index.js ├── tl │ ├── index.h.js │ ├── mediator.js │ ├── type-buffer.js │ └── index.js ├── worker.js ├── store.js ├── error.js ├── crypto.js ├── vendor │ └── .eslintrc └── bin.js ├── dist └── imm.js ├── jsconfig.json ├── .gitignore ├── examples ├── update-profile.js ├── init.js ├── index.js ├── fixtures.js ├── from-readme.js ├── login.js ├── chat-history.js └── node-storage.js ├── .flowconfig ├── logs ├── good.log ├── async fail404.log └── flood.log ├── tsconfig.json ├── .babelrc ├── LICENSE ├── CHANGELOG.md ├── test ├── node.test.js ├── ci-test.js ├── config-validation.js └── layout.test.js ├── index.d.ts ├── package.json ├── README-RU.md ├── README.md ├── .eslintrc └── schema └── mtproto-57.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/** 2 | es/** 3 | dist/** 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const mtproto = require('./src') 2 | 3 | module.exports = mtproto -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webogram: { 3 | main: require('./webogram/webpack.config'), 4 | vendor: main: require('./webogram/webpack.vendor.config') 5 | } 6 | } -------------------------------------------------------------------------------- /src/util/dtime.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | const logTimer = (new Date()).getTime() 4 | 5 | export const dTime = () => `[${(((new Date()).getTime() -logTimer) / 1000).toFixed(3)}]` 6 | 7 | export default dTime -------------------------------------------------------------------------------- /dist/imm.js: -------------------------------------------------------------------------------- 1 | console.info('setImmediate polyfill worker registered') 2 | 3 | // onmessage = function(event) { 4 | // console.info('CW onmessage on message', event.data, event) 5 | // } 6 | // postMessage('ready') -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "allowSyntheticDefaultImports": true 6 | }, 7 | "exclude": [ 8 | "node_modules" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/http.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const httpClient = axios.create() 4 | delete httpClient.defaults.headers.post['Content-Type'] 5 | delete httpClient.defaults.headers.common['Accept'] 6 | 7 | export default httpClient -------------------------------------------------------------------------------- /src/service/updates.h.js: -------------------------------------------------------------------------------- 1 | export type CurState = { 2 | [k: string]: any; 3 | syncPending?: { 4 | ptsAwaiting?: ?boolean; 5 | seqAwaiting?: ?boolean; 6 | } 7 | } 8 | 9 | export type UpdatesState = any; 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .publish 4 | *.sublime-workspace 5 | .vscode 6 | *.*~ 7 | *.swp 8 | 9 | lib/** 10 | es/** 11 | dist/** 12 | debug.log 13 | node_modules 14 | npm-debug.log 15 | cldr 16 | coverage/ 17 | .nyc_output 18 | dist/** 19 | logs/** 20 | typings.json 21 | typings/** -------------------------------------------------------------------------------- /src/service/secure-random.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import RandomBytes from 'randombytes' 4 | 5 | const getRandom = (arr: Array) => { 6 | const ln = arr.length 7 | const buf = RandomBytes(ln) 8 | for (let i = 0; i < ln; i++) 9 | arr[i] = buf[i] 10 | return arr 11 | } 12 | 13 | export default getRandom -------------------------------------------------------------------------------- /examples/update-profile.js: -------------------------------------------------------------------------------- 1 | const telegram = require('./init') 2 | 3 | const updateProfile = async (currentName) => { 4 | const result = await telegram('account.updateProfile', { 5 | first_name: 'lam'//currentName + 'test' 6 | }) 7 | console.log('updateProfile', result) 8 | return result 9 | } 10 | 11 | module.exports = updateProfile -------------------------------------------------------------------------------- /src/service/main/wrap.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Main from './index' 4 | 5 | import type { ConfigType } from './index.h' 6 | import type { ApiManagerInstance } from '../api-manager/index.h' 7 | 8 | function MTProto(config: ConfigType = {}): ApiManagerInstance { 9 | const mtproto = new Main(config) 10 | return mtproto.api 11 | } 12 | 13 | export default MTProto -------------------------------------------------------------------------------- /src/util/switch.js: -------------------------------------------------------------------------------- 1 | export const Switch = (patterns, protector = e => e) => 2 | (matches, mProtector = e => e) => (...data) => { 3 | const keyList = Object.keys(patterns) 4 | const normalized = protector(...data) 5 | for (const key of keyList) 6 | if (patterns[key](normalized)) 7 | return mProtector(matches[key]) 8 | } 9 | 10 | export default Switch -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import MTProto from './service/main/wrap' 2 | 3 | export { CryptoWorker } from './crypto' 4 | export { bin } from './bin' 5 | export { ApiManager } from './service/api-manager/index' 6 | export { setLogger } from './util/log' 7 | 8 | import * as MtpTimeManager from './service/time-manager' 9 | export { MtpTimeManager } 10 | export { MTProto } 11 | export default MTProto -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /lib/**/.* 3 | /es/**/.* 4 | /dist/.* 5 | /logs/.* 6 | /examples/.* 7 | /.vscode/.* 8 | /typings/**/.* 9 | 10 | [include] 11 | 12 | [libs] 13 | 14 | [options] 15 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 16 | suppress_type=$FlowIssue 17 | unsafe.enable_getters_and_setters=true -------------------------------------------------------------------------------- /examples/init.js: -------------------------------------------------------------------------------- 1 | const { MTProto } = require('../lib') 2 | 3 | const api = { 4 | invokeWithLayer: 0xda9b0d0d, 5 | layer : 57, 6 | initConnection : 0x69796de9, 7 | api_id : 49631, 8 | app_version : '1.0.1', 9 | lang_code : 'en' 10 | } 11 | 12 | const server = { webogram: true, dev: true } 13 | 14 | const telegram = MTProto({ api, server }) 15 | 16 | module.exports = telegram -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | const login = require('./login') 2 | const { getChat, chatHistory, searchUsers } = require('./chat-history') 3 | const updateProfile = require('./update-profile') 4 | 5 | const run = async () => { 6 | const first_name = await login() 7 | const res = await searchUsers() 8 | await updateProfile(first_name) 9 | // const chat = await getChat() 10 | // await chatHistory(chat) 11 | } 12 | 13 | run() -------------------------------------------------------------------------------- /examples/fixtures.js: -------------------------------------------------------------------------------- 1 | const prompt = require('prompt') 2 | const input = cfg => new Promise( 3 | (rs, rj) => 4 | prompt.get( 5 | cfg, 6 | (err, res) => err 7 | ? rj(err) 8 | : rs(res))) 9 | 10 | const inputField = field => 11 | input([{ name: field, required: true }]) 12 | .then(res => res[field]) 13 | 14 | prompt.start() 15 | 16 | module.exports = { 17 | input, 18 | inputField 19 | } -------------------------------------------------------------------------------- /src/layout/index.h.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | export type { TLMethod, TLConstruct, TLSchema } from '../tl/index.h' 4 | 5 | export type SchemaParam = { 6 | name: string, 7 | type: string 8 | } 9 | 10 | export type SchemaElement = { 11 | id: string, 12 | type: string, 13 | params: SchemaParam[] 14 | } 15 | 16 | export type TLParam = { 17 | name: string, 18 | typeClass: string, 19 | isVector: boolean, 20 | isFlag: boolean, 21 | flagIndex: number 22 | } -------------------------------------------------------------------------------- /src/util/defer.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird' 2 | /** 3 | * Defered promise like in Q and $q 4 | * 5 | * @returns {{ resolve: (res: any) => void, reject: (res: any) => void, promise: Promise<{}> }} 6 | */ 7 | export const blueDefer = () => { 8 | let resolve, reject 9 | const promise = new Promise((rs, rj) => { 10 | resolve = rs 11 | reject = rj 12 | }) 13 | return { 14 | resolve, 15 | reject, 16 | promise 17 | } 18 | } 19 | 20 | export default blueDefer -------------------------------------------------------------------------------- /src/tl/index.h.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | export type BinaryData = number[] | Uint8Array 4 | 5 | export type TLParam = { 6 | name: string, 7 | type: string 8 | } 9 | 10 | export type TLConstruct = { 11 | id: string, 12 | type: string, 13 | predicate: string, 14 | params: TLParam[] 15 | } 16 | 17 | export type TLMethod = { 18 | id: string, 19 | type: string, 20 | method: string, 21 | params: TLParam[] 22 | } 23 | 24 | export type TLSchema = { 25 | constructors: TLConstruct[], 26 | methods: TLMethod[], 27 | constructorsIndex?: number[] 28 | } -------------------------------------------------------------------------------- /logs/good.log: -------------------------------------------------------------------------------- 1 | [0.445] mtpAuth 2 | [0.491] Send req_pq b24745529ad95c1acc8cc13a265955a8 3 | [0.844] Got ResPQ 734c61de89a6f279c63ed1c6c0b71521 1e46b20b2f87dd81 [ '14101943622620965665' ] 4 | [0.869] PQ factorization start Uint8Array [ 30, 70, 178, 11, 47, 135, 221, 129 ] 5 | [1.793] PQ factorization done 13502 6 | [1.808] Send req_DH_params 7 | [2.020] Done decrypting answer 8 | [2.022] Verifying DH params 9 | [2.026] dhPrime cmp OK 10 | [2.033] 2^{2048-64} < gA < dhPrime-2^{2048-64} OK 11 | [2.035] Apply server time 1487877064 1487877055188 9 false 12 | [2.638] Send set_client_DH_params 13 | [3.523] Got Set_client_DH_params_answer dh_gen_ok -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "*": [ 6 | "src/*" 7 | ] 8 | }, 9 | "outDir": "lib", 10 | "module": "es2015", 11 | "target": "es2015", 12 | "pretty": true, 13 | "sourceMap": true, 14 | "inlineSourceMap": false, 15 | "inlineSources": false, 16 | "experimentalDecorators": false, 17 | "noUnusedParameters": true, 18 | "noUnusedLocals": true, 19 | "moduleResolution": "node", 20 | "lib": [ 21 | "es2015", 22 | "webworker" 23 | ] 24 | }, 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ], 4 | "plugins": [ 5 | "transform-flow-strip-types", 6 | "transform-exponentiation-operator", 7 | ["transform-es2015-for-of", { 8 | "loose": true 9 | }], 10 | "transform-class-properties", 11 | "transform-async-to-generator", 12 | ["transform-es2015-block-scoping", { 13 | "throwIfClosureRequired": true 14 | }], 15 | ["transform-object-rest-spread", { "useBuiltIns": true }] 16 | ,"closure-elimination" 17 | ], 18 | "env": { 19 | "commonjs": { 20 | "plugins": [ 21 | "transform-es2015-modules-commonjs" 22 | ] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /config/webogram/webpack.vendor.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { resolve, join } = require('path') 3 | const webpack = require('webpack') 4 | 5 | const source = resolve(process.cwd(), 'source') 6 | const build = resolve(process.cwd(), 'dist') 7 | 8 | const config = { 9 | devtool: 'source-map', 10 | cache : true, 11 | 12 | entry: { 13 | vendor: [ 14 | // '@goodmind/node-cryptojs-aes', 15 | // 'big-integer', 16 | // 'rusha', 17 | 'pako/lib/inflate', 18 | 'jsbn', 19 | 'axios', 20 | 'ramda', 21 | 'bluebird' 22 | // 'setimmediate' 23 | ] 24 | }, 25 | context: source, 26 | output : { 27 | path : build, 28 | filename: '[name].dll.js', 29 | library : '[name]' 30 | }, 31 | plugins: [ 32 | new webpack.DllPlugin({ 33 | name: '[name]', 34 | path: join(build, '[name].json') 35 | }) 36 | ] 37 | } 38 | 39 | module.exports = config -------------------------------------------------------------------------------- /examples/from-readme.js: -------------------------------------------------------------------------------- 1 | const { MTProto } = require('../lib') 2 | 3 | const phone = { 4 | num : '+9996620001', 5 | code: '22222' 6 | } 7 | 8 | const api = { 9 | layer : 57, 10 | initConnection: 0x69796de9, 11 | api_id : 49631 12 | } 13 | 14 | const server = { 15 | dev: true //We will connect to the test server. 16 | } //Any empty configurations fields can just not be specified 17 | 18 | const client = MTProto({ server, api }) 19 | 20 | async function connect(){ 21 | const { phone_code_hash } = await client('auth.sendCode', { 22 | phone_number : phone.num, 23 | current_number: false, 24 | api_id : 49631, 25 | api_hash : 'fb050b8f6771e15bfda5df2409931569' 26 | }) 27 | const { user } = await client('auth.signIn', { 28 | phone_number: phone.num, 29 | phone_code_hash, 30 | phone_code : phone.code 31 | }) 32 | 33 | console.log('signed as ', user) 34 | } 35 | 36 | connect() -------------------------------------------------------------------------------- /src/service/main/invoke-layer-generator.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | /** 4 | * Defines the parameter required for authorization based on the level number 5 | * 6 | * Values were taken from here 7 | * https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/scheme.tl 8 | * 9 | * @param {number} apiLevel 10 | * @returns 11 | */ 12 | function generateInvokeLayer(apiLevel: number) { 13 | switch (apiLevel) { 14 | case 1: return 0x53835315 15 | case 2: return 0x289dd1f6 16 | case 3: return 0xb7475268 17 | case 4: return 0xdea0d430 18 | case 5: return 0x417a57ae 19 | case 6: return 0x3a64d54d 20 | case 7: return 0xa5be56d3 21 | case 8: return 0xe9abd9fd 22 | case 9: return 0x76715a63 23 | case 10: return 0x39620c41 24 | case 11: return 0xa6b88fdf 25 | case 12: return 0xdda60d3c 26 | case 13: return 0x427c8ea2 27 | case 14: return 0x2b9b08fa 28 | case 15: return 0xb4418b64 29 | case 16: return 0xcf5f0987 30 | case 17: return 0x50858a19 31 | case 18: return 0x1c900537 32 | default: return 0xda9b0d0d 33 | } 34 | } 35 | 36 | export default generateInvokeLayer -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | import { pqPrimeFactorization, bytesModPow, sha1HashSync, 2 | aesEncryptSync, aesDecryptSync } from './bin' 3 | 4 | console.info('Crypto worker registered') 5 | 6 | const runTask = data => { 7 | switch (data.task) { 8 | case 'factorize' : return pqPrimeFactorization(data.bytes) 9 | case 'mod-pow' : return bytesModPow(data.x, data.y, data.m) 10 | case 'sha1-hash' : return sha1HashSync(data.bytes) 11 | case 'aes-encrypt': return aesEncryptSync(data.bytes, data.keyBytes, data.ivBytes) 12 | case 'aes-decrypt': return aesDecryptSync(data.encryptedBytes, 13 | data.keyBytes, data.ivBytes) 14 | default: 15 | throw new Error(`Unknown task: ${data.task}`) 16 | } 17 | } 18 | 19 | onmessage = function(e) { 20 | if (e.data === '') { 21 | console.info('empty crypto task') 22 | } else if (typeof e.data === 'string') { 23 | console.info('crypto task string message', e.data) 24 | } else { 25 | const taskID = e.data.taskID 26 | const result = runTask(e.data) 27 | postMessage({ taskID, result }) 28 | } 29 | } 30 | 31 | postMessage('ready') -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017 Zero Bias https://github.com/zerobias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/service/networker/net-message.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import forEachObjIndexed from 'ramda/src/forEachObjIndexed' 4 | 5 | import { generateID } from '../time-manager' 6 | import blueDefer from '../../util/defer' 7 | 8 | type BodyBytes = number[] | Uint8Array 9 | 10 | export class NetMessage { 11 | body: BodyBytes 12 | seq_no: number 13 | isAPI: boolean 14 | acked: boolean = false 15 | msg_id: string = generateID() 16 | container: boolean = false 17 | deferred = blueDefer() 18 | constructor(seq_no: number, body: BodyBytes) { 19 | this.seq_no = seq_no 20 | this.body = body 21 | } 22 | copyOptions(options: Object) { //TODO remove this 23 | forEachObjIndexed(this.copyHelper, options) 24 | } 25 | copyHelper = (value: any, key: string) => { 26 | //$FlowIssue 27 | this[key] = value 28 | } 29 | size() { 30 | if (this.body instanceof Uint8Array) 31 | return this.body.byteLength 32 | else 33 | return this.body.length 34 | } 35 | } 36 | 37 | export class NetContainer extends NetMessage { 38 | inner: string[] 39 | constructor(seq_no: number, body: BodyBytes, inner: string[]) { 40 | super(seq_no, body) 41 | this.container = true 42 | this.inner = inner 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/login.js: -------------------------------------------------------------------------------- 1 | const telegram = require('./init') 2 | 3 | const { inputField } = require('./fixtures') 4 | 5 | const config = { 6 | // NOTE: if you FORK the project you MUST use your APP ID. 7 | // Otherwise YOUR APPLICATION WILL BE BLOCKED BY TELEGRAM 8 | // You can obtain your own APP ID for your application here: https://my.telegram.org 9 | id : 49631, 10 | hash: 'fb050b8f6771e15bfda5df2409931569' 11 | } 12 | 13 | const login = async () => { 14 | try { 15 | // const phone = await inputField('phone') 16 | // console.log(phone) 17 | const phone = '+9996620000' 18 | const { phone_code_hash } = await telegram('auth.sendCode', { 19 | phone_number : phone, 20 | current_number: false, 21 | api_id : config.id, 22 | api_hash : config.hash 23 | }) 24 | // const code = await inputField('code') 25 | const code = '22222' 26 | const res = await telegram('auth.signIn', { 27 | phone_number: phone, 28 | phone_code_hash, 29 | phone_code : code 30 | }) 31 | const { user } = res 32 | const { 33 | first_name = '', 34 | username = '' 35 | } = user 36 | console.log('signIn', first_name, username, user.phone) 37 | return first_name 38 | } catch (error) { 39 | console.error(error) 40 | } 41 | } 42 | 43 | module.exports = login -------------------------------------------------------------------------------- /src/service/main/index.h.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import type { TLSchema } from '../../tl/index.h' 4 | 5 | export type ApiConfig = { 6 | invokeWithLayer?: number, 7 | layer ?: number, 8 | initConnection ?: number, 9 | api_id ?: number, 10 | device_model ?: string, 11 | system_version ?: string, 12 | app_version ?: string, 13 | lang_code ?: string 14 | } 15 | 16 | export type AsyncStorage = { 17 | get: (...keys: string[]) => Promise, 18 | set: (obj: Object) => Promise, 19 | remove: (...keys: string[]) => Promise, 20 | clear: () => Promise<{}>, 21 | setPrefix: () => void, 22 | noPrefix: () => void 23 | } 24 | 25 | export type PublicKey = { 26 | modulus: string, 27 | exponent: string 28 | } 29 | 30 | export type ConfigType = { 31 | server?: {}, 32 | api?: ApiConfig, 33 | app?: { 34 | storage?: AsyncStorage, 35 | publicKeys?: PublicKey[] 36 | }, 37 | schema?: TLSchema, 38 | mtSchema?: TLSchema, 39 | } 40 | 41 | export type StrictConfig = { 42 | server: {}, 43 | api: ApiConfig, 44 | app: { 45 | storage: AsyncStorage, 46 | publicKeys: PublicKey[] 47 | }, 48 | schema: TLSchema, 49 | mtSchema: TLSchema, 50 | } 51 | 52 | export type Emit = (event: string | string[], ...values: any[]) => boolean 53 | 54 | export type On = (event: string, listener: (...values: any[]) => void) => void -------------------------------------------------------------------------------- /src/util/smart-timeout.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Promise from 'bluebird' 4 | 5 | const cancelToken = Symbol('cancel token') 6 | 7 | const timeoutRefs = new WeakSet 8 | 9 | const pause = (delay: number): Promise => new Promise(r => setTimeout(r, delay)) 10 | 11 | export const smartTimeout = (fn: (...args: Array<*>) => T, delay?: number = 0, ...args: Array<*>) => { 12 | const newToken = Symbol('cancel id') 13 | const checkRun = () => { 14 | if (timeoutRefs.has(newToken)) { 15 | timeoutRefs.delete(newToken) 16 | return fn(...args) 17 | } else return false 18 | } 19 | const promise = pause(delay).then(checkRun) 20 | promise[cancelToken] = newToken 21 | return promise 22 | } 23 | 24 | smartTimeout.cancel = promise => { 25 | if (!promise || !promise[cancelToken]) return false 26 | const token = promise[cancelToken] 27 | return timeoutRefs.has(token) 28 | ? timeoutRefs.delete(token) 29 | : false 30 | } 31 | 32 | export const immediate = (fn: (...args: Array<*>) => T, ...args: Array<*>) => 33 | Promise 34 | .resolve() 35 | .then(() => fn(...args)) 36 | 37 | 38 | export const delayedCall = 39 | (fn: (...args: Array<*>) => T, delay?: number = 0, ...args: Array<*>) => 40 | pause(delay) 41 | .then(() => fn(...args)) 42 | 43 | smartTimeout.immediate = immediate 44 | smartTimeout.promise = delayedCall 45 | 46 | export default smartTimeout -------------------------------------------------------------------------------- /src/service/api-manager/index.h.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import type { Emit, On } from '../main/index.h' 4 | 5 | export type Bytes = number[] 6 | 7 | export type PublicKey = { //TODO remove this 8 | modulus: string, 9 | exponent: string 10 | } 11 | 12 | export type LeftOptions = { 13 | dcID?: number, 14 | createNetworker?: boolean, 15 | fileDownload?: boolean, 16 | fileUpload?: boolean 17 | } 18 | 19 | export type AsyncStorage = { //TODO remove this 20 | get(...keys: string[]): Promise, 21 | set(obj: Object): Promise, 22 | remove(...keys: string[]): Promise, 23 | clear(): Promise<{}>, 24 | setPrefix(): void, 25 | noPrefix(): void 26 | } 27 | 28 | export type Cached = { 29 | [id: number]: Model 30 | } 31 | 32 | export type NetworkerType = { 33 | wrapApiCall: (method: string, params?: Object, options?: LeftOptions) => Promise 34 | } 35 | 36 | export type Cache = { 37 | uploader: Cached, 38 | downloader: Cached, 39 | auth: Cached<*>, 40 | servers: Cached<*>, 41 | keysParsed: Cached, 42 | } 43 | 44 | export type ApiManagerInstance = { 45 | (method: string): Promise, 46 | (method: string, params: Object): Promise, 47 | (method: string, params: Object, options: Object): Promise, 48 | storage: AsyncStorage, 49 | setUserAuth(dc: number, userAuth: any): void, 50 | on: On, 51 | emit: Emit 52 | } 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.2.2 2 | * Fixed bug with cyrillic text encoding 3 | * Fixed bug with several broken methods invocations 4 | * Optimized authorization performance 5 | * Added fix for automatic base data center selection by [@goodmind][] 6 | 7 | # 2.2.1 8 | * Flag (optional type) fields now acts like common fields. 9 | * Zeroes, empty strings and empty types now can be omited. write only useful fields. 10 | * *invokeWithLayer* api field now may detects internally and don't required (but still valid). 11 | * Type check argument fields 12 | * Fix auth race condition 13 | * Add batch async logger 14 | 15 | # 2.2.0 16 | 17 | * **breaking** Instance now creates without `new` 18 | * **breaking** Rename module exports from `ApiManager` to `MTProto` 19 | 20 | # =<2.1.0 21 | 22 | Several early alpha versions based on new architechture 23 | 24 | --- 25 | 26 | # 1.1.0 *(beta)* 27 | 28 | * **breaking** Remove all functions from response. Just use the field values. 29 | * Remove logger from response 30 | * Add changelog.md 31 | 32 | # 1.0.6 *(beta)* 33 | 34 | * Https connection. Usage: 35 | ```javascript 36 | const { network } = require('telegram-mtproto') 37 | const connection = network.http({ host: 'ip', port: '80', protocol: 'https' }) 38 | ``` 39 | * Websockets connection. Usage: 40 | ```javascript 41 | const connection = network.wc({ host: 'ip', port: '80' }) 42 | ``` 43 | * Precision timing 44 | * Major performance boost 45 | 46 | [@goodmind]: https://github.com/goodmind/ -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird' 2 | 3 | import clone from 'ramda/src/clone' 4 | 5 | export const ValueStore = () => { 6 | let val = null 7 | 8 | return { 9 | get: () => clone(val), 10 | set: newVal => val = newVal 11 | } 12 | } 13 | 14 | export const ValueStoreMap = () => { 15 | const val = new Map 16 | 17 | return { 18 | get: key => clone(val.get(key)), 19 | set: (key, newVal) => val.set(key, newVal) 20 | } 21 | } 22 | 23 | export const TimeOffset = ValueStore() 24 | export const dcList = ValueStoreMap() 25 | 26 | export const AsyncStorage = () => { 27 | const store = new Map 28 | 29 | const get = key => store.get(key) 30 | const set = (key, val) => store.set(key, val) 31 | const remove = keys => keys.map(e => store.delete(e)) 32 | const clr = () => store.clear() 33 | return { 34 | get : (key) => Promise.resolve(get(key)), 35 | set : (key, val) => Promise.resolve(set(key, val)), 36 | remove : (...keys) => Promise.resolve(remove(keys)), 37 | clear : () => Promise.resolve(clr()), 38 | noPrefix: () => ({}), 39 | store 40 | } 41 | } 42 | 43 | export const PureStorage = AsyncStorage() /*{ 44 | get : (...keys) => new Promise(rs => ConfigStorage.get(keys, rs)), 45 | set : obj => new Promise(rs => ConfigStorage.set(obj, rs)), 46 | remove : (...keys) => new Promise(rs => ConfigStorage.remove(...keys, rs)), 47 | noPrefix: () => ConfigStorage.noPrefix(), 48 | clear : () => new Promise(rs => ConfigStorage.clear(rs)) 49 | }*/ 50 | -------------------------------------------------------------------------------- /src/service/networker/state.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | // import Promise from 'bluebird' 4 | 5 | import { NetMessage } from './net-message' 6 | 7 | type MessageMap = Map 8 | type PendingMap = Map 9 | type ResendSet = Set 10 | 11 | class Pool { 12 | sent: MessageMap = new Map() 13 | pending: PendingMap = new Map() 14 | resend: ResendSet = new Set() 15 | addResend(msg_id: string) { 16 | return this.resend.add(msg_id) 17 | } 18 | hasResends() { 19 | return this.resend.size > 0 20 | } 21 | deleteResent(msg_id: string) { 22 | return this.resend.delete(msg_id) 23 | } 24 | getResends() { 25 | return this.resend.values() 26 | } 27 | 28 | 29 | addSent(message: NetMessage) { 30 | this.sent.set(message.msg_id, message) 31 | } 32 | getSent(msg_id: string): NetMessage { 33 | //$FlowIssue 34 | return this.sent.get(msg_id) 35 | } 36 | hasSent(msg_id: string) { 37 | return this.sent.has(msg_id) 38 | } 39 | deleteSent(message: NetMessage) { 40 | return this.sent.delete(message.msg_id) 41 | } 42 | sentIterator() { 43 | return this.sent.entries() 44 | } 45 | 46 | setPending(msg_id: string, value: number = 0) { 47 | return this.pending.set(msg_id, value) 48 | } 49 | hasPending(msg_id: string) { 50 | return this.pending.has(msg_id) 51 | } 52 | deletePending(msg_id: string) { 53 | return this.pending.delete(msg_id) 54 | } 55 | pendingIterator() { 56 | return this.pending.entries() 57 | } 58 | } 59 | 60 | export default Pool -------------------------------------------------------------------------------- /src/service/time-manager.js: -------------------------------------------------------------------------------- 1 | import isNode from 'detect-node' 2 | 3 | import { TimeOffset } from '../store' 4 | import { nextRandomInt, lshift32 } from '../bin' 5 | 6 | import Logger from '../util/log' 7 | 8 | const log = Logger`time-manager` 9 | 10 | export const tsNow = seconds => { 11 | let t = +new Date() 12 | //eslint-disable-next-line 13 | if (!isNode) t += window.tsOffset || 0 14 | return seconds 15 | ? Math.floor(t / 1000) 16 | : t 17 | } 18 | 19 | export { dTime } from '../util/dtime' 20 | 21 | let lastMessageID = [0, 0] 22 | let timerOffset = 0 23 | 24 | const offset = TimeOffset.get() 25 | if (offset) timerOffset = offset 26 | 27 | const generateMessageID = () => { 28 | const timeTicks = tsNow(), 29 | timeSec = Math.floor(timeTicks / 1000) + timerOffset, 30 | timeMSec = timeTicks % 1000, 31 | random = nextRandomInt(0xFFFF) 32 | 33 | let messageID = [timeSec, timeMSec << 21 | random << 3 | 4] 34 | if (lastMessageID[0] > messageID[0] || 35 | lastMessageID[0] == messageID[0] && lastMessageID[1] >= messageID[1]) { 36 | messageID = [lastMessageID[0], lastMessageID[1] + 4] 37 | } 38 | 39 | lastMessageID = messageID 40 | 41 | // console.log('generated msg id', messageID, timerOffset) 42 | 43 | return lshift32(messageID[0], messageID[1]) 44 | } 45 | 46 | export const applyServerTime = (serverTime, localTime) => { 47 | const newTimeOffset = serverTime - Math.floor((localTime || tsNow()) / 1000) 48 | const changed = Math.abs(timerOffset - newTimeOffset) > 10 49 | TimeOffset.set(newTimeOffset) 50 | 51 | lastMessageID = [0, 0] 52 | timerOffset = newTimeOffset 53 | log('Apply server time')(serverTime, localTime, newTimeOffset, changed) 54 | 55 | return changed 56 | } 57 | 58 | export { generateMessageID as generateID } 59 | -------------------------------------------------------------------------------- /test/node.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('tap') 2 | const { MTProto } = require('../lib') 3 | 4 | const phone = { 5 | num : '+9996620000', 6 | code: '22222' 7 | } 8 | 9 | const api = { 10 | // invokeWithLayer: 0xda9b0d0d, 11 | layer : 57, 12 | initConnection: 0x69796de9, 13 | api_id : 49631, 14 | app_version : '1.0.1', 15 | lang_code : 'en' 16 | } 17 | const server = { 18 | dev : true, 19 | webogram: false 20 | } 21 | 22 | const config = { 23 | // NOTE: if you FORK the project you MUST use your APP ID. 24 | // Otherwise YOUR APPLICATION WILL BE BLOCKED BY TELEGRAM 25 | // You can obtain your own APP ID for your application here: https://my.telegram.org 26 | id : 49631, 27 | hash: 'fb050b8f6771e15bfda5df2409931569' 28 | } 29 | 30 | 31 | const telegram = MTProto({ server, api, app: { } }) 32 | 33 | test(`Connection test`, async t => { 34 | t.plan(1) 35 | const run = async () => { 36 | let res, i = 0 37 | while (i<5) { 38 | try { 39 | const { phone_code_hash } = await telegram('auth.sendCode', { 40 | phone_number : phone.num, 41 | current_number: false, 42 | api_id : config.id, 43 | api_hash : config.hash 44 | }) 45 | console.log('phone_code_hash', phone_code_hash) 46 | res = await telegram('auth.signIn', { 47 | phone_number: phone.num, 48 | phone_code_hash, 49 | phone_code : phone.code 50 | }) 51 | // console.log('signIn', res) 52 | console.log('\n Logined as user') 53 | console.dir(res.user, { colors: true }) 54 | t.ok(res, 'result is ok') 55 | break 56 | } catch (err) { 57 | console.log('err', err) 58 | } 59 | i++ 60 | } 61 | } 62 | await run() 63 | }).then(() => { 64 | process.exit(0) 65 | }) -------------------------------------------------------------------------------- /src/service/rsa-keys-manger.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Promise from 'bluebird' 4 | 5 | import type { PublicKey } from './main/index.h' 6 | import type { Cached } from './api-manager/index.h' 7 | import type { SerializationFabric } from '../tl' 8 | 9 | import { WriteMediator } from '../tl' 10 | 11 | import { bytesToHex, sha1BytesSync, 12 | bytesFromHex, strDecToHex } from '../bin' 13 | 14 | 15 | export const KeyManager = (Serialization: SerializationFabric, 16 | publicKeysHex: PublicKey[], 17 | publicKeysParsed: Cached) => { 18 | let prepared = false 19 | 20 | const mapPrepare = ({ modulus, exponent }: PublicKey) => { 21 | const RSAPublicKey = Serialization() 22 | const rsaBox = RSAPublicKey.writer 23 | WriteMediator.bytes(rsaBox, bytesFromHex(modulus), 'n') 24 | WriteMediator.bytes(rsaBox, bytesFromHex(exponent), 'e') 25 | 26 | const buffer = rsaBox.getBuffer() 27 | 28 | const fingerprintBytes = sha1BytesSync(buffer).slice(-8) 29 | fingerprintBytes.reverse() 30 | 31 | publicKeysParsed[bytesToHex(fingerprintBytes)] = { 32 | modulus, 33 | exponent 34 | } 35 | } 36 | 37 | async function prepareRsaKeys() { 38 | if (prepared) return 39 | 40 | await Promise.map(publicKeysHex, mapPrepare) 41 | 42 | prepared = true 43 | } 44 | 45 | async function selectRsaKeyByFingerPrint(fingerprints: string[]) { 46 | await prepareRsaKeys() 47 | 48 | let fingerprintHex, foundKey 49 | for (const fingerprint of fingerprints) { 50 | fingerprintHex = strDecToHex(fingerprint) 51 | foundKey = publicKeysParsed[fingerprintHex] 52 | if (foundKey) 53 | return { fingerprint, ...foundKey } 54 | } 55 | return false 56 | } 57 | 58 | return { 59 | prepare: prepareRsaKeys, 60 | select : selectRsaKeyByFingerPrint 61 | } 62 | } 63 | 64 | export default KeyManager 65 | -------------------------------------------------------------------------------- /src/service/dc-configurator.js: -------------------------------------------------------------------------------- 1 | import has from 'ramda/src/has' 2 | import propEq from 'ramda/src/propEq' 3 | import find from 'ramda/src/find' 4 | import pipe from 'ramda/src/pipe' 5 | import prop from 'ramda/src/prop' 6 | 7 | const sslSubdomains = ['pluto', 'venus', 'aurora', 'vesta', 'flora'] 8 | 9 | const devDC = [ 10 | { id: 1, host: '149.154.175.10', port: 80 }, 11 | { id: 2, host: '149.154.167.40', port: 80 }, 12 | { id: 3, host: '149.154.175.117', port: 80 } 13 | ] 14 | 15 | const prodDC = [ 16 | { id: 1, host: '149.154.175.50', port: 80 }, 17 | { id: 2, host: '149.154.167.51', port: 80 }, 18 | { id: 3, host: '149.154.175.100', port: 80 }, 19 | { id: 4, host: '149.154.167.91', port: 80 }, 20 | { id: 5, host: '149.154.171.5', port: 80 } 21 | ] 22 | 23 | const portString = ({ port = 80 }) => port === 80 24 | ? '' 25 | : `:${port}` 26 | 27 | const findById = pipe( propEq('id'), find ) 28 | 29 | export const chooseServer = (chosenServers, { 30 | dev = false, 31 | webogram = false, 32 | dcList = dev 33 | ? devDC 34 | : prodDC 35 | } = {}) => 36 | (dcID, upload = false) => { 37 | const choosen = prop(dcID) 38 | if (has(dcID, chosenServers)) return choosen(chosenServers) 39 | let chosenServer = false 40 | 41 | 42 | if (webogram) { 43 | const subdomain = sslSubdomains[dcID - 1] + (upload ? '-1' : '') 44 | const path = dev 45 | ? 'apiw_test1' 46 | : 'apiw1' 47 | chosenServer = `https://${ subdomain }.web.telegram.org/${ path }` 48 | return chosenServer //TODO Possibly bug. Isn't it necessary? chosenServers[dcID] = chosenServer 49 | } 50 | const dcOption = findById(parseInt(dcID))(dcList) 51 | if (dcOption) 52 | chosenServer = `http://${ dcOption.host }${portString(dcOption)}/apiw1` 53 | chosenServers[dcID] = chosenServer 54 | 55 | return choosen(chosenServers) 56 | } -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import type { TypeBuffer } from './tl/type-buffer' 4 | 5 | export class MTError extends Error { 6 | static getMessage(code: number, type: string, message: string) { 7 | return `MT[${code}] ${type}: ${message}` 8 | } 9 | code: number 10 | type: string 11 | constructor(code: number, type: string, message: string) { 12 | const fullMessage = MTError.getMessage(code, type, message) 13 | super(fullMessage) 14 | this.code = code 15 | this.type = type 16 | } 17 | } 18 | 19 | export class ErrorBadResponse extends MTError { 20 | originalError: Error 21 | constructor(url: string, originalError?: Error | null = null) { 22 | super(406, 'NETWORK_BAD_RESPONSE', url) 23 | if (originalError) 24 | this.originalError = originalError 25 | } 26 | } 27 | 28 | export class ErrorBadRequest extends MTError { 29 | originalError: Error 30 | constructor(url: string, originalError?: Error | null = null) { 31 | super(406, 'NETWORK_BAD_REQUEST', url) 32 | if (originalError) 33 | this.originalError = originalError 34 | } 35 | } 36 | 37 | export class ErrorNotFound extends MTError { 38 | constructor(err: Object) { 39 | super(404, 'REQUEST_FAILED', err.config.url) 40 | // this.originalError = err 41 | } 42 | } 43 | 44 | export class TypeBufferIntError extends MTError { 45 | static getTypeBufferMessage(ctx: TypeBuffer) { 46 | const offset = ctx.offset 47 | const length = ctx.intView.length * 4 48 | return `Can not get next int: offset ${offset} length: ${length}` 49 | } 50 | typeBuffer: TypeBuffer 51 | constructor(ctx: TypeBuffer) { 52 | const message = TypeBufferIntError.getTypeBufferMessage(ctx) 53 | super(800, 'NO_NEXT_INT', message) 54 | this.typeBuffer = ctx 55 | } 56 | } 57 | 58 | export class AuthKeyError extends MTError { 59 | constructor() { 60 | super(401, 'AUTH_KEY_EMPTY', '') 61 | } 62 | } -------------------------------------------------------------------------------- /config/webogram/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { resolve, join } = require('path') 3 | const webpack = require('webpack') 4 | 5 | const source = resolve(process.cwd(), 'source') 6 | const build = resolve(process.cwd(), 'dist') 7 | 8 | const vendorDll = require(join(build, 'vendor.json')) 9 | 10 | const config = { 11 | devtool: 'source-map', 12 | cache : true, 13 | 14 | entry : './index.js', 15 | context: source, 16 | resolve: { 17 | extensions: ['.js'] 18 | // modules : [ 19 | // source, 20 | // 'node_modules' 21 | // ] 22 | }, 23 | output: { 24 | filename : 'mtproto2-browser.js', 25 | // publicPath: '/', 26 | path : build, 27 | library : 'mtproto', 28 | libraryTarget: 'umd' 29 | // pathinfo : true 30 | }, 31 | externals: { 32 | // 'bluebird': { 33 | // commonjs : 'lodash', 34 | // commonjs2: 'lodash', 35 | // amd : 'lodash', 36 | // root : 'Promise' 37 | // } 38 | // ramda: { 39 | // commonjs: 'ramda', 40 | // umd : 'ramda', 41 | // root : 'R' 42 | // }, 43 | // jsbn: { 44 | // commonjs: 'jsbn', 45 | // umd : 'jsbn', 46 | // root : 'jsbn' 47 | // }, 48 | // axios: { 49 | // commonjs: 'axios', 50 | // umd : 'axios', 51 | // root : 'axios' 52 | // } 53 | }, 54 | plugins: [ 55 | new webpack.DllReferencePlugin({ 56 | context : source, 57 | manifest: vendorDll 58 | }), 59 | new webpack.LoaderOptionsPlugin({ 60 | options: { 61 | worker: { 62 | output: { 63 | filename: 'hash.worker.js', 64 | chunkFilename: '[id].hash.worker.js' 65 | } 66 | } 67 | } 68 | }) 69 | ], 70 | module: { 71 | rules: [{ 72 | test : /\.js$/, 73 | exclude: /node_modules/, 74 | loader : 'babel-loader' 75 | }] 76 | } 77 | } 78 | 79 | module.exports = config -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'telegram-mtproto' { 2 | type DC = { 3 | id: number 4 | host: string 5 | port: number 6 | } 7 | type ServerConfig = { 8 | dev?: boolean 9 | webogram?: boolean 10 | dcList?: Array 11 | } 12 | type PublicKey = { 13 | modulus: string 14 | exponent: string 15 | } 16 | type AppConfig = { 17 | debug?: boolean 18 | publicKeys?: Array 19 | storage?: AsyncStorage 20 | } 21 | type ApiConfig = { 22 | invokeWithLayer?: number 23 | layer ?: number 24 | initConnection ?: number 25 | api_id ?: number 26 | device_model ?: string 27 | system_version ?: string 28 | app_version ?: string 29 | lang_code ?: string 30 | } 31 | type Config = { 32 | server?: ServerConfig 33 | api?: ApiConfig 34 | app?: AppConfig 35 | schema?: Object 36 | mtSchema?: Object 37 | } 38 | 39 | export interface ApiManagerInstance { 40 | readonly storage: AsyncStorage 41 | readonly updates: any 42 | (method: string): Promise 43 | (method: string, params: Object): Promise 44 | (method: string, params: Object, options: Object): Promise 45 | setUserAuth(dc: number, userAuth: T): void 46 | on(event: string|string[], handler: Function): void 47 | } 48 | interface IApiManager { 49 | new (): ApiManagerInstance 50 | new ({ server, api, app, schema, mtSchema }: Config): ApiManagerInstance 51 | } 52 | export const ApiManager: IApiManager 53 | class ApiManagerClass { 54 | readonly storage: AsyncStorage 55 | setUserAuth(dc: number, userAuth: T): void 56 | on(event: string|string[], handler: Function): void 57 | } 58 | export interface AsyncStorage { 59 | get(key: string): Promise 60 | set(key: string, val: any): Promise 61 | remove(...keys: string[]): Promise 62 | clear(): Promise<{}> 63 | } 64 | 65 | function MTProto({ server, api, app, schema, mtSchema }: Config): ApiManagerInstance 66 | export default MTProto 67 | } 68 | -------------------------------------------------------------------------------- /examples/chat-history.js: -------------------------------------------------------------------------------- 1 | const { pluck } = require('ramda') 2 | const { inputField } = require('./fixtures') 3 | 4 | const telegram = require('./init') 5 | 6 | const getChat = async () => { 7 | const dialogs = await telegram('messages.getDialogs', { 8 | limit: 50, 9 | }) 10 | const { chats } = dialogs 11 | const selectedChat = await selectChat(chats) 12 | 13 | return selectedChat 14 | } 15 | 16 | const chatHistory = async chat => { 17 | const max = 400 18 | const limit = 100 19 | let offset = 0 20 | let full = [], 21 | messages = [] 22 | do { 23 | const history = await telegram('messages.getHistory', { 24 | peer: { 25 | _ : 'inputPeerChannel', 26 | channel_id : chat.id, 27 | access_hash: chat.access_hash 28 | }, 29 | max_id: offset, 30 | offset: -full.length, 31 | limit 32 | }) 33 | messages = history.messages.filter(filterLastDay) 34 | full = full.concat(messages) 35 | messages.length > 0 && (offset = messages[0].id) 36 | messages.length > 0 && console.log(offset, messages[0].id) 37 | } while (messages.length === limit && full.length < max) 38 | printMessages(full) 39 | return full 40 | } 41 | 42 | const filterLastDay = ({ date }) => new Date(date*1e3) > dayRange() 43 | 44 | const dayRange = () => Date.now() - new Date(86400000*4) 45 | 46 | const selectChat = async (chats) => { 47 | const chatNames = pluck('title', chats) 48 | console.log('Your chat list') 49 | chatNames.map((name, id) => console.log(`${id} ${name}`)) 50 | console.log('Select chat by index') 51 | const chatIndex = await inputField('index') 52 | return chats[+chatIndex] 53 | } 54 | 55 | const filterUsersMessages = ({ _ }) => _ === 'message' 56 | 57 | const formatMessage = ({ message, date, from_id }) => { 58 | const dt = new Date(date*1e3) 59 | const hours = dt.getHours() 60 | const mins = dt.getMinutes() 61 | return `${hours}:${mins} [${from_id}] ${message}` 62 | } 63 | 64 | const printMessages = messages => { 65 | const filteredMsg = messages.filter(filterUsersMessages) 66 | const formatted = filteredMsg.map(formatMessage) 67 | formatted.forEach(e => console.log(e)) 68 | return formatted 69 | } 70 | 71 | 72 | const searchUsers = async (username) => { 73 | const results = await telegram('contacts.search', { 74 | q : username, 75 | limit: 100, 76 | }) 77 | return results 78 | } 79 | 80 | module.exports = { 81 | getChat, 82 | chatHistory, 83 | searchUsers 84 | } -------------------------------------------------------------------------------- /test/ci-test.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const Nightmare = require('nightmare') 3 | 4 | const night = Nightmare({ 5 | openDevTools: { 6 | mode: 'detach' 7 | }, 8 | show : true, 9 | executionTimeout: 120e3 10 | }) 11 | 12 | const data = { 13 | phoneCode: '+9', 14 | phone : '996620000', 15 | code : '22222', 16 | 17 | url : 'http://localhost:8000/', 18 | sel1: { 19 | country: '[ng-model="credentials.phone_country"]', 20 | phone : '[ng-model="credentials.phone_number"]', 21 | code : '[ng-model="credentials.phone_code"]', 22 | next : '.login_head_submit_btn', 23 | ok : '[ng-click="$close()"]', 24 | form : '.login_page', 25 | end : '[my-i18n="im_select_a_chat"]', 26 | err : '[my-i18n="error_modal_internal_title"]', 27 | im : 'Please select a chat to start messaging' 28 | } 29 | } 30 | 31 | const fieldFastEdit = function(sel) { 32 | document.querySelector(sel).value = '' 33 | return document.querySelector(sel).value 34 | } 35 | test('try login', t => { 36 | t.notThrow(async () => { 37 | let count = 0, 38 | done = false 39 | await night 40 | .goto(data.url) 41 | while (count < 5 && !done) { 42 | const isError = await night 43 | .wait(data.sel1.country) 44 | .wait(8e3) 45 | .visible(data.sel1.err) 46 | console.log('succ', isError) 47 | done = !isError 48 | if (isError) 49 | await night 50 | .refresh() 51 | count++ 52 | } 53 | await night 54 | // .goto(data.url) 55 | // .wait(data.sel1.country) 56 | // .wait(8e3) 57 | // // .click(data.sel1.country) 58 | .evaluate(fieldFastEdit, data.sel1.country) 59 | .insert( 60 | data.sel1.country, 61 | data.phoneCode) 62 | .insert( 63 | data.sel1.phone, 64 | data.phone) 65 | .click(data.sel1.next) 66 | .wait(data.sel1.ok) 67 | .wait(2e3) 68 | .click(data.sel1.ok) 69 | .wait(data.sel1.code) 70 | .wait(2e3) 71 | .type(data.sel1.code, data.code) 72 | .wait(data.sel1.end) 73 | .wait(9e3) 74 | const text = await night 75 | .evaluate(function(sel){ 76 | return document.querySelector(sel).innerText 77 | }, data.sel1.end) 78 | if (text === data.sel1.im) 79 | await night.end() 80 | // .retype(data.sel1.country, '+9') 81 | // .then(e => { 82 | console.warn('done', text) 83 | 84 | t.equal(text, data.sel1.im, 'valid welcome message') 85 | t.end() 86 | // process.exit(0) 87 | // }) 88 | }, 'smoke test') 89 | }) -------------------------------------------------------------------------------- /src/service/main/config-validation.js: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv' 2 | import AjvKeys from 'ajv-keywords/keywords/typeof' 3 | import propIs from 'ramda/src/propIs' 4 | 5 | const type = { 6 | func: { typeof: 'function' }, 7 | num : { type: 'number' }, 8 | str : { type: 'string' }, 9 | bool: { type: 'boolean' }, 10 | obj : { type: 'object' } 11 | } 12 | 13 | const app = { 14 | type : 'object', 15 | properties: { 16 | publicKeys: { 17 | type : 'array', 18 | uniqueItems: true, 19 | }, 20 | storage: { 21 | type : 'object', 22 | required : ['get', 'set', 'remove', 'clear'], 23 | properties: { 24 | get : type.func, 25 | set : type.func, 26 | remove: type.func, 27 | clear : type.func 28 | }, 29 | additionalProperties: true 30 | } 31 | }, 32 | additionalProperties: false 33 | } 34 | 35 | const api = { 36 | type : 'object', 37 | required : ['layer', 'api_id'], 38 | properties: { 39 | invokeWithLayer: type.num, 40 | layer : type.num, 41 | initConnection : type.num, 42 | api_id : type.num, 43 | device_model : type.str, 44 | system_version : type.str, 45 | app_version : type.str, 46 | lang_code : type.str 47 | }, 48 | additionalProperties: false 49 | } 50 | 51 | const dc = { 52 | type : 'object', 53 | required : ['id', 'host'], 54 | properties: { 55 | id : type.num, 56 | host: type.str, 57 | port: type.num 58 | }, 59 | additionalProperties: false 60 | } 61 | 62 | const server = { 63 | type : 'object', 64 | properties: { 65 | dev : type.bool, 66 | webogram: type.bool, 67 | dcList : { 68 | type : 'array', 69 | uniqueItems: true, 70 | items : dc 71 | } 72 | }, 73 | additionalProperties: false 74 | } 75 | 76 | const schema = { 77 | properties: { 78 | app, 79 | api, 80 | server, 81 | schema : type.obj, 82 | mtSchema: type.obj 83 | }, 84 | additionalProperties: false 85 | } 86 | 87 | const ajv = new Ajv() 88 | AjvKeys(ajv) 89 | const validate = ajv.compile(schema) 90 | 91 | const configValidator = config => { 92 | const valid = validate(config) 93 | if (!valid) { 94 | console.log('config errors') 95 | validate.errors.map(printObj) 96 | throw new Error('wrong config fields') 97 | } 98 | } 99 | 100 | const canDir = propIs(Function, 'dir', console) 101 | const printObj = canDir 102 | ? (arg) => console.dir( arg, { colors: true }) 103 | : (arg) => console.log(arg) 104 | 105 | 106 | export default configValidator -------------------------------------------------------------------------------- /examples/node-storage.js: -------------------------------------------------------------------------------- 1 | const { MTProto } = require('telegram-mtproto') 2 | const { Storage } = require('mtproto-storage-fs') 3 | const readline = require('readline') 4 | 5 | // The api_id and api_hash values can be obtained here: https://my.telegram.org/ 6 | const config = { 7 | "phone_number": "+1xxxxxxxxx", 8 | "api_id": 00000, 9 | "api_hash": "xxxxxx" 10 | } 11 | 12 | const app = { 13 | storage: new Storage('./storage.json') 14 | } 15 | 16 | const phone = { 17 | num: config.phone_number 18 | } 19 | 20 | const api = { 21 | layer : 57, 22 | initConnection: 0x69796de9, 23 | api_id : config.api_id 24 | } 25 | 26 | const server = { 27 | dev: false 28 | } 29 | 30 | const client = MTProto({ server, api, app }) 31 | 32 | // This function will stop execution of the program until you enter the code 33 | // that is sent via SMS or Telegram. 34 | const askForCode = () => { 35 | return new Promise((resolve) => { 36 | const rl = readline.createInterface({ 37 | input : process.stdin, 38 | output: process.stdout 39 | }) 40 | 41 | rl.question('Please enter passcode for ' + phone.num + ':\n', (num) => { 42 | rl.close() 43 | resolve(num) 44 | }) 45 | }) 46 | } 47 | 48 | // First you will receive a code via SMS or Telegram, which you have to enter 49 | // directly in the command line. If you entered the correct code, you will be 50 | // logged in and the credentials are saved. 51 | const login = async (client, phone) => { 52 | const { phone_code_hash } = await client('auth.sendCode', { 53 | phone_number : phone.num, 54 | current_number: false, 55 | api_id : config.api_id, 56 | api_hash : config.api_hash 57 | }) 58 | 59 | const phone_code = await askForCode() 60 | console.log(`Your code: ${phone_code}`) 61 | 62 | const { user } = await client('auth.signIn', { 63 | phone_number : phone.num, 64 | phone_code_hash: phone_code_hash, 65 | phone_code : phone_code 66 | }) 67 | 68 | console.log('signed as ', user) 69 | } 70 | 71 | const getDialogs = async () => { 72 | const dialogs = await client('messages.getDialogs', { 73 | limit: 100, 74 | }) 75 | console.log('dialogs', dialogs) 76 | } 77 | 78 | // First check if we are already signed in (if credentials are stored). If we 79 | // are logged in, execution continues, otherwise the login process begins. 80 | (async function() { 81 | if (!(await app.storage.get('signedin'))) { 82 | console.log('not signed in') 83 | 84 | await login(client, phone).catch(console.error) 85 | 86 | console.log('signed in successfully') 87 | app.storage.set('signedin', true) 88 | } else { 89 | console.log('already signed in') 90 | } 91 | getDialogs() 92 | })() -------------------------------------------------------------------------------- /src/service/api-manager/request.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Promise from 'bluebird' 4 | 5 | import Logger from '../../util/log' 6 | const debug = Logger([`request`]) 7 | 8 | import { MTError } from '../../error' 9 | import { delayedCall } from '../../util/smart-timeout' 10 | import type { NetworkerType, AsyncStorage, LeftOptions } from './index.h.js' 11 | 12 | type Options = {| 13 | networker?: NetworkerType, 14 | dc: number, 15 | storage: AsyncStorage, 16 | getNetworker: (dcID: number, options: LeftOptions) => Promise, 17 | netOpts: mixed 18 | |} 19 | 20 | class Request { 21 | method: string 22 | params: { [arg: string]: mixed } 23 | config: Options 24 | constructor(config: Options, method: string, params?: Object = {}) { 25 | this.config = config 26 | this.method = method 27 | this.params = params 28 | 29 | this.performRequest = this.performRequest.bind(this) 30 | //$FlowIssue 31 | this.error303 = this.error303.bind(this) 32 | //$FlowIssue 33 | this.error420 = this.error420.bind(this) 34 | this.initNetworker = this.initNetworker.bind(this) 35 | } 36 | initNetworker = (): Promise => { 37 | if (!this.config.networker) { 38 | const { getNetworker, netOpts, dc } = this.config 39 | return getNetworker(dc, netOpts) 40 | .then(this.saveNetworker) 41 | } 42 | return Promise.resolve(this.config.networker) 43 | } 44 | saveNetworker = (networker: NetworkerType) => this.config.networker = networker 45 | performRequest = () => this.initNetworker().then(this.requestWith) 46 | requestWith = (networker: NetworkerType) => networker 47 | .wrapApiCall(this.method, this.params, this.config.netOpts) 48 | .catch({ code: 303 }, this.error303) 49 | .catch({ code: 420 }, this.error420) 50 | error303(err: MTError) { 51 | const matched = err.type.match(/^(PHONE_MIGRATE_|NETWORK_MIGRATE_|USER_MIGRATE_)(\d+)/) 52 | if (!matched || matched.length < 2) return Promise.reject(err) 53 | const [ , , newDcID] = matched 54 | if (+newDcID === this.config.dc) return Promise.reject(err) 55 | this.config.dc = +newDcID 56 | delete this.config.networker 57 | /*if (this.config.dc) 58 | this.config.dc = newDcID 59 | else 60 | await this.config.storage.set('dc', newDcID)*/ 61 | //TODO There is disabled ability to change default DC 62 | //NOTE Shouldn't we must reassign current networker/cachedNetworker? 63 | return this.performRequest() 64 | } 65 | error420(err: MTError) { 66 | const matched = err.type.match(/^FLOOD_WAIT_(\d+)/) 67 | if (!matched || matched.length < 2) return Promise.reject(err) 68 | const [ , waitTime ] = matched 69 | console.error(`Flood error! The mtproto server has banned you for ${waitTime} seconds.`) 70 | return +waitTime > 60 71 | ? Promise.reject(err) 72 | : delayedCall(this.performRequest, +waitTime * 1e3) 73 | } 74 | } 75 | 76 | export default Request 77 | -------------------------------------------------------------------------------- /src/service/authorizer/send-plain-req.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Promise from 'bluebird' 4 | 5 | import has from 'ramda/src/has' 6 | import pathEq from 'ramda/src/pathEq' 7 | import allPass from 'ramda/src/allPass' 8 | 9 | import httpClient from '../../http' 10 | import { ErrorBadResponse, ErrorNotFound } from '../../error' 11 | import { generateID } from '../time-manager' 12 | import { WriteMediator, ReadMediator } from '../../tl' 13 | 14 | import type { TLFabric } from '../../tl' 15 | 16 | const is404 = pathEq(['response', 'status'], 404) 17 | const notError = allPass([has('message'), has('type')]) 18 | 19 | const SendPlain = ({ Serialization, Deserialization }: TLFabric) => { 20 | const onlySendPlainReq = (url: string, requestBuffer: ArrayBuffer) => { 21 | const requestLength = requestBuffer.byteLength, 22 | requestArray = new Int32Array(requestBuffer) 23 | 24 | const header = Serialization() 25 | const headBox = header.writer 26 | 27 | WriteMediator.longP(headBox, 0, 0, 'auth_key_id') // Auth key 28 | WriteMediator.long(headBox, generateID(), 'msg_id') // Msg_id 29 | WriteMediator.int(headBox, requestLength, 'request_length') 30 | 31 | const headerBuffer: ArrayBuffer = headBox.getBuffer(), 32 | headerArray = new Int32Array(headerBuffer) 33 | const headerLength = headerBuffer.byteLength 34 | 35 | const resultBuffer = new ArrayBuffer(headerLength + requestLength), 36 | resultArray = new Int32Array(resultBuffer) 37 | 38 | resultArray.set(headerArray) 39 | resultArray.set(requestArray, headerArray.length) 40 | 41 | const requestData = resultArray 42 | // let reqPromise 43 | // try { 44 | const reqPromise = httpClient.post(url, requestData, { 45 | responseType: 'arraybuffer' 46 | }) 47 | // } catch (e) { 48 | // reqPromise = Promise.reject(new ErrorBadResponse(url, e)) 49 | // } 50 | return Promise.props({ url, req: reqPromise }) 51 | } 52 | 53 | const onlySendPlainErr = (err) => { 54 | let error 55 | switch (true) { 56 | case is404(err): 57 | error = new ErrorNotFound(err) 58 | break 59 | case notError(err): 60 | error = new ErrorBadResponse('', err) 61 | break 62 | default: 63 | error = err 64 | } 65 | return Promise.reject(error) 66 | } 67 | 68 | const onlySendPlainRes = ({ url, req }) => { 69 | if (!req.data || !req.data.byteLength) 70 | return Promise.reject(new ErrorBadResponse(url)) 71 | let deserializer 72 | try { 73 | deserializer = Deserialization(req.data, { mtproto: true }) 74 | const ctx = deserializer.typeBuffer 75 | ReadMediator.long(ctx, 'auth_key_id') 76 | ReadMediator.long(ctx, 'msg_id') 77 | ReadMediator.int(ctx, 'msg_len') 78 | } catch (e) { 79 | return Promise.reject(new ErrorBadResponse(url, e)) 80 | } 81 | 82 | return deserializer 83 | } 84 | 85 | const sendPlainReq = (url: string, requestBuffer: ArrayBuffer) => 86 | onlySendPlainReq(url, requestBuffer) 87 | .then( 88 | onlySendPlainRes, 89 | onlySendPlainErr) 90 | 91 | return sendPlainReq 92 | } 93 | 94 | export default SendPlain -------------------------------------------------------------------------------- /test/config-validation.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | 3 | const Ajv = require('ajv') 4 | const AjvKeys = require('ajv-keywords/keywords/typeof') 5 | 6 | const type = { 7 | func: { typeof: 'function' }, 8 | num : { type: 'number' }, 9 | str : { type: 'string' }, 10 | bool: { type: 'boolean' } 11 | } 12 | 13 | const app = { 14 | type : 'object', 15 | properties: { 16 | storage: { 17 | type : 'object', 18 | required : ['get', 'set', 'remove', 'clear'], 19 | properties: { 20 | get : type.func, 21 | set : type.func, 22 | remove: type.func, 23 | clear : type.func 24 | }, 25 | additionalProperties: true 26 | } 27 | }, 28 | additionalProperties: false 29 | } 30 | 31 | const api = { 32 | type : 'object', 33 | required : ['layer', 'api_id'], 34 | properties: { 35 | invokeWithLayer: type.num, 36 | layer : type.num, 37 | initConnection : type.num, 38 | api_id : type.num, 39 | device_model : type.str, 40 | system_version : type.str, 41 | app_version : type.str, 42 | lang_code : type.str 43 | }, 44 | additionalProperties: false 45 | } 46 | 47 | const dc = { 48 | type : 'object', 49 | required : ['id', 'host'], 50 | properties: { 51 | id : type.num, 52 | host: type.str, 53 | port: type.num 54 | }, 55 | additionalProperties: false 56 | } 57 | 58 | const server = { 59 | type : 'object', 60 | properties: { 61 | dev : type.bool, 62 | webogram: type.bool, 63 | dcList : { 64 | type : 'array', 65 | uniqueItems: true, 66 | items : dc 67 | } 68 | }, 69 | additionalProperties: false 70 | } 71 | 72 | const schema = { 73 | properties: { 74 | app, 75 | api, 76 | server, 77 | schema : { type: 'object' }, 78 | mtSchema: { type: 'object' } 79 | }, 80 | additionalProperties: false 81 | } 82 | 83 | test('full module config validation', t => { 84 | t.plan(1) 85 | const ajv = new Ajv() 86 | AjvKeys(ajv) 87 | 88 | const validate = ajv.compile(schema) 89 | 90 | const opts = { 91 | app: { 92 | storage: { 93 | get() {}, 94 | set() {}, 95 | remove() {}, 96 | //eslint-disable-next-line 97 | clear: () => {} 98 | } 99 | }, 100 | api: { 101 | invokeWithLayer: 0xda9b0d0d, 102 | layer : 57, 103 | initConnection : 0x69796de9, 104 | api_id : 49631, 105 | device_model : 'Unknown UserAgent', 106 | system_version : 'Unknown Platform', 107 | app_version : '1.0.1', 108 | lang_code : 'en' 109 | }, 110 | server: { 111 | dev : true, 112 | webogram: true, 113 | dcList : [ 114 | { id: 1, host: '149.154.175.10', port: 80 }, 115 | { id: 2, host: '149.154.167.40', port: 80 }, 116 | { id: 3, host: '149.154.175.117', port: 80 } 117 | ] 118 | } 119 | } 120 | 121 | const valid = validate(opts) 122 | 123 | const print = res => res 124 | ? res 125 | : validate.errors 126 | 127 | console.log(print(valid)) 128 | t.equal(valid, true, 'validation result is true') 129 | }) -------------------------------------------------------------------------------- /src/util/log.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | // import memoize from 'memoizee' 4 | 5 | import Debug from 'debug' 6 | 7 | import trim from 'ramda/src/trim' 8 | import map from 'ramda/src/map' 9 | import chain from 'ramda/src/chain' 10 | import pipe from 'ramda/src/pipe' 11 | import split from 'ramda/src/split' 12 | import both from 'ramda/src/both' 13 | import is from 'ramda/src/is' 14 | import when from 'ramda/src/when' 15 | import take from 'ramda/src/take' 16 | import reject from 'ramda/src/reject' 17 | import isEmpty from 'ramda/src/isEmpty' 18 | import join from 'ramda/src/join' 19 | import unapply from 'ramda/src/unapply' 20 | import unnest from 'ramda/src/unnest' 21 | import tail from 'ramda/src/tail' 22 | 23 | import dTime from './dtime' 24 | import { immediate } from './smart-timeout' 25 | 26 | type VariString = string | string[] 27 | 28 | type Normalize = (tag: string) => string 29 | type FullNormalize = (tags: VariString) => string 30 | 31 | const tagNormalize: Normalize = e => `[${e}]` 32 | 33 | const arrify = unapply(unnest) 34 | 35 | const fullNormalize: FullNormalize = pipe( 36 | arrify, 37 | chain(split(',')), 38 | map(trim), 39 | reject(isEmpty), 40 | map(tagNormalize), 41 | join('') 42 | ) 43 | 44 | const stringNormalize = when( 45 | both(is(String), e => e.length > 50), 46 | take(150) 47 | ) 48 | // const isSimple = either( 49 | // is(String), 50 | // is(Number) 51 | // ) 52 | 53 | // const prettify = unless( 54 | // isSimple, 55 | // pretty 56 | // ) 57 | 58 | const genericLogger = Debug('telegram-mtproto') 59 | 60 | class LogEvent { 61 | log: typeof genericLogger 62 | values: mixed[] 63 | constructor(log: typeof genericLogger, values: mixed[]) { 64 | this.log = log 65 | this.values = values 66 | } 67 | print() { 68 | this.log(...this.values) 69 | } 70 | } 71 | 72 | class Sheduler { 73 | queue: LogEvent[][] = [] 74 | buffer: LogEvent[] = [] 75 | add = (log: typeof genericLogger, time: string, tagStr: string, values: mixed[]) => { 76 | const results = values.map(stringNormalize) 77 | const first = results[0] || '' 78 | const other = tail(results) 79 | const firstLine = [tagStr, time, first].join(' ') 80 | this.buffer.push(new LogEvent(log, [firstLine, ...other])) 81 | } 82 | sheduleBuffer = () => { 83 | this.queue.push(this.buffer) 84 | this.buffer = [] 85 | } 86 | print = () => { 87 | for (const buffer of this.queue) 88 | for (const logEvent of buffer) 89 | logEvent.print() 90 | this.queue = [] 91 | } 92 | constructor() { 93 | setInterval(this.sheduleBuffer, 50) 94 | setInterval(this.print, 300) 95 | } 96 | } 97 | 98 | const sheduler = new Sheduler 99 | 100 | const Logger = (moduleName: VariString, ...rest: string[]) => { 101 | const fullModule: string[] = arrify(moduleName, ...rest) 102 | fullModule.unshift('telegram-mtproto') 103 | const fullname = fullModule.join(':') 104 | const debug = Debug(fullname) 105 | const logger = (tags: string | string[]) => { 106 | const tagStr = fullNormalize(tags) 107 | return (...objects: any[]) => { 108 | const time = dTime() 109 | immediate(sheduler.add, debug, time, tagStr, objects) 110 | } 111 | } 112 | return logger 113 | } 114 | 115 | export const setLogger = (customLogger: Function) => { 116 | Debug.log = customLogger 117 | } 118 | 119 | export default Logger -------------------------------------------------------------------------------- /src/service/main/index.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import EventEmitter from 'eventemitter2' 4 | 5 | import { ApiManager } from '../api-manager' 6 | import NetworkerFabric from '../networker' 7 | import { PureStorage } from '../../store' 8 | import TL from '../../tl' 9 | 10 | import configValidator from './config-validation' 11 | import generateInvokeLayer from './invoke-layer-generator' 12 | 13 | import type { TLFabric } from '../../tl' 14 | import type { ApiConfig, ConfigType, StrictConfig, Emit, On, PublicKey } from './index.h' 15 | import type { ApiManagerInstance } from '../api-manager/index.h' 16 | 17 | const api57 = require('../../../schema/api-57.json') 18 | const mtproto57 = require('../../../schema/mtproto-57.json') 19 | 20 | const apiConfig: ApiConfig = { 21 | invokeWithLayer: 0xda9b0d0d, 22 | layer : 57, 23 | initConnection : 0x69796de9, 24 | api_id : 49631, 25 | device_model : 'Unknown UserAgent', 26 | system_version : 'Unknown Platform', 27 | app_version : '1.0.1', 28 | lang_code : 'en' 29 | } 30 | 31 | class MTProto { 32 | config: StrictConfig 33 | tls: TLFabric 34 | emitter = new EventEmitter({ 35 | wildcard: true 36 | }) 37 | api: ApiManagerInstance 38 | on: On = this.emitter.on.bind(this.emitter) 39 | emit: Emit = this.emitter.emit.bind(this.emitter) 40 | constructor(config: ConfigType) { 41 | this.config = configNormalization(config) 42 | this.tls = TL(this.config.schema, this.config.mtSchema) 43 | const netFabric = NetworkerFabric(this.config.api, this.tls, this.config.app.storage, this.emit) 44 | this.api = new ApiManager(this.config, this.tls, netFabric, { on: this.on, emit: this.emit }) 45 | } 46 | } 47 | 48 | export default MTProto 49 | 50 | const configNormalization = (config: ConfigType): StrictConfig => { 51 | const { 52 | server = {}, 53 | api = {}, 54 | app: { 55 | storage = PureStorage, 56 | publicKeys = publicKeysHex 57 | } = {}, 58 | schema = api57, 59 | mtSchema = mtproto57, 60 | } = config 61 | const apiNormalized = { ...apiConfig, ...api } 62 | const invokeLayer = generateInvokeLayer(apiNormalized.layer) 63 | apiNormalized.invokeWithLayer = invokeLayer 64 | const fullCfg = { 65 | server, 66 | api: apiNormalized, 67 | app: { storage, publicKeys }, 68 | schema, 69 | mtSchema 70 | } 71 | configValidator(fullCfg) 72 | return fullCfg 73 | } 74 | 75 | /** 76 | * Server public key, obtained from here: https://core.telegram.org/api/obtaining_api_id 77 | * 78 | * -----BEGIN RSA PUBLIC KEY----- 79 | * MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6 80 | * lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS 81 | * an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw 82 | * Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+ 83 | * 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n 84 | * Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB 85 | * -----END RSA PUBLIC KEY----- 86 | */ 87 | 88 | const publicKeysHex: PublicKey[] = [{ 89 | modulus: 90 | 'c150023e2f70db7985ded064759cfecf0af328e69a41daf4d6f01b538135a6f91f' + 91 | '8f8b2a0ec9ba9720ce352efcf6c5680ffc424bd634864902de0b4bd6d49f4e5802' + 92 | '30e3ae97d95c8b19442b3c0a10d8f5633fecedd6926a7f6dab0ddb7d457f9ea81b' + 93 | '8465fcd6fffeed114011df91c059caedaf97625f6c96ecc74725556934ef781d86' + 94 | '6b34f011fce4d835a090196e9a5f0e4449af7eb697ddb9076494ca5f81104a305b' + 95 | '6dd27665722c46b60e5df680fb16b210607ef217652e60236c255f6a28315f4083' + 96 | 'a96791d7214bf64c1df4fd0db1944fb26a2a57031b32eee64ad15a8ba68885cde7' + 97 | '4a5bfc920f6abf59ba5c75506373e7130f9042da922179251f', 98 | exponent: '010001' 99 | }] 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Telegram MTProto library", 3 | "name": "telegram-mtproto", 4 | "license": "MIT", 5 | "main": "lib/index.js", 6 | "jsnext:main": "es/index.js", 7 | "module": "es/index.js", 8 | "typings": "index.d.ts", 9 | "scripts": { 10 | "build": "npm run build:cjs && npm run build:es", 11 | "watch:node": "npm run build:cjs -- --watch", 12 | "watch:web": "webpack --watch --config config/webogram/webpack.config.js", 13 | "build:webogram:vendor": "webpack --config config/webogram/webpack.vendor.config.js", 14 | "build:webogram": "webpack --config config/webogram/webpack.config.js", 15 | "rebuild:webogram": "npm run build:webogram:vendor && npm run build:webogram", 16 | "rebuild:cjs": "npm run clean:cjs & npm run build:cjs", 17 | "build:cjs": "cross-env BABEL_ENV=commonjs babel src/ -d lib -s", 18 | "build:async": "cross-env BABEL_ENV=commonjs babel src/ -d lib -s --no-babelrc --plugins=transform-flow-strip-types,transform-es2015-modules-commonjs,transform-class-properties,transform-object-rest-spread,closure-elimination", 19 | "clean:cjs": "rimraf lib/", 20 | "rebuild:es": "npm run clean:es & npm run build:es", 21 | "build:es": "babel src/ -d es -s", 22 | "clean:es": "rimraf es/", 23 | "test": "node --harmony test/node.test.js", 24 | "prepublish": "npm run rebuild:es && npm run rebuild:cjs" 25 | }, 26 | "author": "Zero Bias", 27 | "dependencies": { 28 | "@goodmind/node-cryptojs-aes": "^0.5.0", 29 | "ajv": "^4.11.5", 30 | "ajv-keywords": "^1.5.1", 31 | "axios": "^0.15.3", 32 | "bluebird": "^3.5.0", 33 | "debug": "^2.6.3", 34 | "detect-node": "^2.0.3", 35 | "eventemitter2": "^3.0.2", 36 | "memoizee": "^0.4.4", 37 | "pako": "^1.0.4", 38 | "ramda": "^0.23.0", 39 | "randombytes": "^2.0.3", 40 | "rusha": "^0.8.5", 41 | "worker-loader": "^0.8.0" 42 | }, 43 | "devDependencies": { 44 | "@types/debug": "0.0.29", 45 | "@types/memoizee": "^0.4.0", 46 | "babel-cli": "^6.24.0", 47 | "babel-core": "^6.24.0", 48 | "babel-eslint": "^7.2.0", 49 | "babel-loader": "^6.4.1", 50 | "babel-plugin-closure-elimination": "^1.1.14", 51 | "babel-plugin-transform-async-to-generator": "^6.22.0", 52 | "babel-plugin-transform-class-properties": "^6.23.0", 53 | "babel-plugin-transform-es2015-block-scoping": "^6.23.0", 54 | "babel-plugin-transform-es2015-for-of": "^6.23.0", 55 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", 56 | "babel-plugin-transform-exponentiation-operator": "^6.22.0", 57 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 58 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 59 | "cross-env": "^3.2.4", 60 | "eslint": "^3.18.0", 61 | "eslint-plugin-babel": "^4.1.1", 62 | "eslint-plugin-flowtype": "^2.30.4", 63 | "eslint-plugin-promise": "^3.5.0", 64 | "flow-bin": "^0.42.0", 65 | "hirestime": "^3.1.0", 66 | "nightmare": "^2.10.0", 67 | "prompt": "^1.0.0", 68 | "rimraf": "^2.6.1", 69 | "tap": "^10.3.0", 70 | "webpack": "^2.2.1" 71 | }, 72 | "engines": { 73 | "node": ">=6.0" 74 | }, 75 | "bugs": { 76 | "url": "https://github.com/zerobias/telegram-mtproto/issues" 77 | }, 78 | "files": [ 79 | "lib", 80 | "test", 81 | "examples", 82 | "schema", 83 | "source", 84 | "es", 85 | "index.d.ts" 86 | ], 87 | "keywords": [ 88 | "telegram", 89 | "mt", 90 | "mtproto", 91 | "mobile", 92 | "protocol", 93 | "library" 94 | ], 95 | "maintainers": [ 96 | { 97 | "name": "Zero Bias", 98 | "email": "ribkatt@gmail.com" 99 | } 100 | ], 101 | "readme": "README.md", 102 | "repository": { 103 | "type": "git", 104 | "url": "git+https://github.com/zerobias/telegram-mtproto.git" 105 | }, 106 | "version": "2.2.2" 107 | } 108 | -------------------------------------------------------------------------------- /src/service/api-manager/error-cases.js: -------------------------------------------------------------------------------- 1 | import isNil from 'ramda/src/isNil' 2 | import propOr from 'ramda/src/propOr' 3 | 4 | import blueDefer from '../../util/defer' 5 | import Switch from '../../util/switch' 6 | import { tsNow } from '../time-manager' 7 | 8 | const cachedExportPromise = {} 9 | 10 | const protect = ( 11 | { code = NaN, type = '' }, 12 | { rawError = null }, 13 | dcID, 14 | baseDcID 15 | ) => ({ 16 | base: baseDcID, 17 | errR: rawError, 18 | code, 19 | type, 20 | dcID 21 | }) 22 | 23 | const patterns = { 24 | noBaseAuth: ({ code, dcID, base }) => code === 401 && dcID === base, 25 | noDcAuth : ({ code, dcID, base }) => code === 401 && dcID !== base, 26 | waitFail : ({ code, type, errR }) => !errR && (code === 500 || type === 'MSG_WAIT_FAILED'), 27 | _ : () => true 28 | } 29 | 30 | 31 | const matchProtect = 32 | matched => ( 33 | error, 34 | options, 35 | dcID, 36 | emit, 37 | rejectPromise, 38 | requestThunk, 39 | apiSavedNet, 40 | apiRecall, 41 | deferResolve, 42 | mtpInvokeApi, 43 | storage 44 | ) => 45 | matched({ 46 | invoke : mtpInvokeApi, 47 | throwNext: () => rejectPromise(error), 48 | reject : rejectPromise, 49 | options, 50 | dcID, 51 | emit, 52 | requestThunk, 53 | apiRecall, 54 | deferResolve, 55 | apiSavedNet, 56 | storage 57 | }) 58 | 59 | 60 | const noBaseAuth = ({ emit, throwNext, storage }) => { 61 | storage.remove('dc', 'user_auth') 62 | emit('error.401.base') 63 | throwNext() 64 | } 65 | 66 | const noDcAuth = ({ dcID, reject, apiSavedNet, apiRecall, deferResolve, invoke }) => { 67 | const importAuth = ({ id, bytes }) => invoke( 68 | 'auth.importAuthorization', 69 | { id, bytes }, 70 | { dcID, noErrorBox: true }) 71 | 72 | 73 | if (isNil(cachedExportPromise[dcID])) { 74 | const exportDeferred = blueDefer() 75 | 76 | invoke( 77 | 'auth.exportAuthorization', 78 | { dc_id: dcID }, 79 | { noErrorBox: true }) 80 | 81 | .then(importAuth) 82 | .then(exportDeferred.resolve) 83 | .catch(exportDeferred.reject) 84 | 85 | cachedExportPromise[dcID] = exportDeferred.promise 86 | } 87 | 88 | 89 | 90 | cachedExportPromise[dcID] //TODO not returning promise 91 | .then(apiSavedNet) 92 | .then(apiRecall) 93 | .then(deferResolve) 94 | .catch(reject) 95 | } 96 | /* 97 | const migrate = ({ error, dcID, options, reject, 98 | apiRecall, deferResolve, getNet, storage 99 | }) => { 100 | const newDcID = error.type.match(/^(PHONE_MIGRATE_|NETWORK_MIGRATE_|USER_MIGRATE_)(\d+)/)[2] 101 | if (newDcID === dcID) return 102 | if (options.dcID) 103 | options.dcID = newDcID 104 | else 105 | storage.set('dc', newDcID) 106 | 107 | getNet(newDcID, options) 108 | .then(apiRecall) 109 | .then(deferResolve) 110 | .catch(reject) 111 | }*/ 112 | 113 | /*const floodWait = ({ error, options, throwNext, requestThunk }) => { 114 | const waitTime = error.type.match(/^FLOOD_WAIT_(\d+)/)[1] || 10 115 | if (waitTime > (options.timeout || 60)) 116 | return throwNext() 117 | requestThunk(waitTime) 118 | }*/ 119 | 120 | const waitFail = ({ options, throwNext, requestThunk }) => { 121 | const now = tsNow() 122 | if (options.stopTime) { 123 | if (now >= options.stopTime) 124 | return throwNext() 125 | } else 126 | options.stopTime = now + propOr(10, 'timeout', options) * 1000 127 | options.waitTime = options.waitTime 128 | ? Math.min(60, options.waitTime * 1.5) 129 | : 1 130 | requestThunk(options.waitTime) 131 | } 132 | 133 | const def = ({ throwNext }) => throwNext() 134 | 135 | 136 | export const switchErrors = Switch(patterns, protect)({ 137 | noBaseAuth, 138 | noDcAuth, 139 | waitFail, 140 | _: def 141 | }, matchProtect) -------------------------------------------------------------------------------- /test/layout.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('tap') 2 | const { getFlags, Layout } = require('../lib/layout') 3 | const apiSchema = require('../schema/api-57.json') 4 | 5 | const { has } = require('ramda') 6 | 7 | const methodRaw = { 8 | 'id' : '-1137057461', 9 | 'method': 'messages.saveDraft', 10 | 'params': [{ 11 | 'name': 'flags', 12 | 'type': '#' 13 | }, { 14 | 'name': 'no_webpage', 15 | 'type': 'flags.1?true' 16 | }, { 17 | 'name': 'reply_to_msg_id', 18 | 'type': 'flags.0?int' 19 | }, { 20 | 'name': 'peer', 21 | 'type': 'InputPeer' 22 | }, { 23 | 'name': 'message', 24 | 'type': 'string' 25 | }, { 26 | 'name': 'entities', 27 | 'type': 'flags.3?Vector' 28 | }], 29 | 'type': 'Bool' 30 | } 31 | 32 | 33 | const methodModel = { 34 | id : -1137057461, 35 | name : 'messages.saveDraft', 36 | returns : 'Bool', 37 | hasFlags: true, 38 | params : [ 39 | { 40 | name : 'flags', 41 | type : '#', 42 | isVector : false, 43 | isFlag : false, 44 | flagIndex: NaN 45 | }, { 46 | name : 'no_webpage', 47 | type : 'true', 48 | isVector : false, 49 | isFlag : true, 50 | flagIndex: 1 51 | }, { 52 | name : 'reply_to_msg_id', 53 | type : 'int', 54 | isVector : false, 55 | isFlag : true, 56 | flagIndex: 0 57 | }, { 58 | name : 'peer', 59 | type : 'InputPeer', 60 | isVector : false, 61 | isFlag : false, 62 | flagIndex: NaN 63 | }, { 64 | name : 'message', 65 | type : 'string', 66 | isVector : false, 67 | isFlag : false, 68 | flagIndex: NaN 69 | }, { 70 | name : 'entities', 71 | type : 'MessageEntity', 72 | isVector : true, 73 | isFlag : true, 74 | flagIndex: 3 75 | }, 76 | ] 77 | } 78 | 79 | const MessageEntity = { 80 | name : 'MessageEntity', 81 | creators: [ 82 | 'messageEntityUnknown', 83 | 'messageEntityMention', 84 | 'messageEntityHashtag' 85 | ] 86 | } 87 | 88 | const entityCreator1 = { 89 | 'id' : '-1148011883', 90 | 'predicate': 'messageEntityUnknown', 91 | 'params' : [ 92 | { 93 | 'name': 'offset', 94 | 'type': 'int' 95 | }, { 96 | 'name': 'length', 97 | 'type': 'int' 98 | } 99 | ], 100 | 'type': 'MessageEntity' 101 | } 102 | const entityCreator2 = { 103 | 'id' : '-100378723', 104 | 'predicate': 'messageEntityMention', 105 | 'params' : [ 106 | { 107 | 'name': 'offset', 108 | 'type': 'int' 109 | }, { 110 | 'name': 'length', 111 | 'type': 'int' 112 | } 113 | ], 114 | 'type': 'MessageEntity' 115 | } 116 | const entityCreator3 = { 117 | 'id' : '1868782349', 118 | 'predicate': 'messageEntityHashtag', 119 | 'params' : [ 120 | { 121 | 'name': 'offset', 122 | 'type': 'int' 123 | }, { 124 | 'name': 'length', 125 | 'type': 'int' 126 | } 127 | ], 128 | 'type': 'MessageEntity' 129 | } 130 | 131 | 132 | const entityCreatorModel = { 133 | id : -1148011883, 134 | name : 'messageEntityUnknown', 135 | type : 'MessageEntity', 136 | hasFlags: false, 137 | params : [ 138 | { 139 | name : 'offset', 140 | type : 'int', 141 | isVector : false, 142 | isFlag : false, 143 | flagIndex: NaN 144 | }, { 145 | name : 'length', 146 | type : 'int', 147 | isVector : false, 148 | isFlag : false, 149 | flagIndex: NaN 150 | }, 151 | ] 152 | } 153 | 154 | /*const method = Layout.method('auth.sendCode') 155 | for (const param of method.params) { 156 | 157 | }*/ 158 | test('flags counter', t => { 159 | const flagTests = [ 160 | [{}, 0], 161 | [{ 162 | no_webpage: true 163 | }, 2], 164 | [{ 165 | no_webpage : true, 166 | reply_to_msg_id: 1234556 167 | }, 3], 168 | [{ 169 | entities : [{}], 170 | reply_to_msg_id: 1234556 171 | }, 9], 172 | ] 173 | 174 | 175 | let getter 176 | t.notThrow(() => getter = getFlags(methodModel), 'getFlags') 177 | for (const [ obj, result ] of flagTests) 178 | t.equal(getter(obj), result, `flags test`) 179 | let layout 180 | t.notThrow(() => layout = new Layout(apiSchema), 'make new layout') 181 | // console.log(layout) 182 | t.end() 183 | }) -------------------------------------------------------------------------------- /README-RU.md: -------------------------------------------------------------------------------- 1 | # telegram-mtproto 2 | 3 | [![npm version][npm-image]][npm-url] 4 | 5 | **Mobile Telegram Protocol** [(MTProto)](https://core.telegram.org/mtproto) пакет написан на **ES6** 6 | 7 | ## Про MTProto... 8 | 9 | **MTProto** - это протокол сервиса [Telegram](http://www.telegram.org) _"придуман для работы с серверами API из мобильных приложений"_. 10 | 11 | Mobile Protocol подразделятся на 3 части ([с официального сайта](https://core.telegram.org/mtproto#general-description)): 12 | 13 | - **Компонент высшего уровня (API query language):** определяет методы через которые запросы и ответы преобразуются в двоичные сообщения. 14 | - **Криптографический слой (для авторизаций):** определяет методы через которые сообщения шифруются перед передачей. 15 | - **Транспортный компонент:** определяет протоколы по которым будут общаться клиент и сервер (такие как, `http`, `https`, `tcp`, `udp`). 16 | 17 | 18 | ## Про telegram-mtproto в двух словах... 19 | 20 | Никакие другие пакеты не нужны. 21 | Пакет **telegram-mtproto** позволяет реализовать все возможности работы с **Mobile Protocol**: 22 | 23 | - Выший уровень API для подключения к серверу 24 | 25 | - API которая работает через промисы 26 | 27 | - **HTTP подключения** которые выполняют перенос данных 28 | 29 | - **Web worker** - сверх быстрые криптографическеские работы в фоновом режиме 30 | 31 | - Реализация **AES и RSA-шифрования** шифрования, для безопасности 32 | 33 | - Оба метода **plain-text** и **encrypted message** для передачи данных на сервер 34 | 35 | - Обмен **ключами Диффи-Хеллмана**, поддерживающими функцию **prime factorization**, для безопасности 36 | 37 | - **MTProto TL-Schema** сборник **JavaScript классов и функций** 38 | 39 | - **async storage** для сохранения данных между сессиями 40 | 41 | 42 | ## Установка 43 | 44 | ```bash 45 | $ npm install --save telegram-mtproto@beta 46 | ``` 47 | 48 | ## Использование 49 | 50 | ```javascript 51 | import MTProto from 'telegram-mtproto' 52 | 53 | const phone = { 54 | num : '+79123456789', 55 | code: '22222' 56 | } 57 | 58 | const api = { 59 | layer : 57, 60 | initConnection : 0x69796de9, 61 | api_id : 49631 62 | } 63 | 64 | const server = { 65 | dev: true // Подключаемся к тестовому серверу. 66 | } // Пустые поля конфигураций можно не указывать 67 | 68 | const client = MTProto({ server, api }) 69 | 70 | async function connect(){ 71 | const { phone_code_hash } = await client('auth.sendCode', { 72 | phone_number : phone.num, 73 | current_number: false, 74 | api_id : 49631, 75 | api_hash : 'fb050b8f6771e15bfda5df2409931569' 76 | }) 77 | const { user } = await client('auth.signIn', { 78 | phone_number : phone.num, 79 | phone_code_hash: phone_code_hash, 80 | phone_code : phone.code 81 | }) 82 | 83 | console.log('Вы вошли как ', user) 84 | } 85 | 86 | connect() 87 | ``` 88 | 89 | Выше мы использовали две функции из API. 90 | 91 | ```typescript 92 | type auth.sendCode = ( 93 | phone_number: string, 94 | sms_type: int, 95 | api_id: int, 96 | api_hash: string, 97 | lang_code: string 98 | ) => { 99 | phone_registered: boolean, 100 | phone_code_hash: string, 101 | send_call_timeout: int, 102 | is_password: boolean 103 | } 104 | 105 | type auth.signIn = ( 106 | phone_number: string, 107 | phone_code_hash: string, 108 | phone_code: string 109 | ) => { 110 | expires: int, 111 | user: User 112 | } 113 | ``` 114 | [Больше][send-code] про [авторизацию][sign-in], а также о многих других методах, вы можете прочитать в [документации][docs]. 115 | 116 | Дополнительные примеры можно посмотреть в [папке с примерами][examples]. 117 | 118 | ## Хранение 119 | 120 | Вы можете использовать наши основные хранилища [localForage][localForage] для хранения данных. 121 | Модуль имеет следующие интерфейсы: 122 | 123 | ```typescript 124 | interface AsyncStorage { 125 | get(key: string): Promise; 126 | set(key: string, value: any): Promise; 127 | remove(...keys: string[]): Promise; 128 | clear(): Promise; 129 | } 130 | ``` 131 | 132 | ```javascript 133 | import { MTProto } from 'telegram-mtproto' 134 | import { api } from './config' 135 | import CustomStorage from './storage' 136 | 137 | const client = MTProto({ 138 | api, 139 | app: { 140 | storage: CustomStorage 141 | } 142 | }) 143 | 144 | ``` 145 | 146 | ## Лицензия 147 | 148 | Проект запущен под лицензией [MIT](./LICENSE) 149 | 150 | [examples]: https://github.com/zerobias/telegram-mtproto/tree/develop/examples 151 | [localForage]: https://github.com/localForage/localForage 152 | [docs]: https://core.telegram.org/ 153 | [send-code]: https://core.telegram.org/method/auth.sendCode 154 | [sign-in]: https://core.telegram.org/method/auth.signIn 155 | [npm-url]: https://www.npmjs.org/package/telegram-mtproto 156 | [npm-image]: https://badge.fury.io/js/telegram-mtproto.svg 157 | -------------------------------------------------------------------------------- /src/crypto.js: -------------------------------------------------------------------------------- 1 | import when from 'ramda/src/when' 2 | import is from 'ramda/src/is' 3 | import identity from 'ramda/src/identity' 4 | import has from 'ramda/src/has' 5 | import both from 'ramda/src/both' 6 | import isNode from 'detect-node' 7 | 8 | import blueDefer from './util/defer' 9 | import smartTimeout from './util/smart-timeout' 10 | import { convertToUint8Array, sha1HashSync, sha256HashSync, 11 | aesEncryptSync, aesDecryptSync, convertToByteArray, convertToArrayBuffer, 12 | pqPrimeFactorization, bytesModPow } from './bin' 13 | 14 | const convertIfArray = when(is(Array), convertToUint8Array) 15 | let webWorker = !isNode 16 | let taskID = 0 17 | const awaiting = {} 18 | const webCrypto = isNode 19 | ? false 20 | //eslint-disable-next-line 21 | : window.crypto.subtle || window.crypto.webkitSubtle //TODO remove browser depends 22 | //eslint-disable-next-line 23 | || window.msCrypto && window.msCrypto.subtle 24 | const useWebCrypto = webCrypto && !!webCrypto.digest 25 | let useSha1Crypto = useWebCrypto 26 | let useSha256Crypto = useWebCrypto 27 | const finalizeTask = (taskID, result) => { 28 | const deferred = awaiting[taskID] 29 | if (deferred) { 30 | // console.log(rework_d_T(), 'CW done') 31 | deferred.resolve(result) //TODO Possibly, can be used as 32 | delete awaiting[taskID] // 33 | } // deferred = Promise.resolve() 34 | } // deferred.resolve( result ) 35 | 36 | const isCryptoTask = both(has('taskID'), has('result')) 37 | 38 | //eslint-disable-next-line 39 | const workerEnable = !isNode && window.Worker 40 | if (workerEnable) { 41 | const TmpWorker = require('worker-loader?inline!./worker.js') 42 | const tmpWorker = new TmpWorker() 43 | // tmpWorker.onmessage = function(event) { 44 | // console.info('CW tmpWorker.onmessage', event && event.data) 45 | // } 46 | tmpWorker.onmessage = e => { 47 | if (e.data === 'ready') { 48 | console.info('CW ready') 49 | } else if (!isCryptoTask(e.data)) { 50 | console.info('Not crypto task', e, e.data) 51 | return e 52 | } else 53 | return webWorker 54 | ? finalizeTask(e.data.taskID, e.data.result) 55 | : webWorker = tmpWorker 56 | } 57 | 58 | tmpWorker.onerror = function(error) { 59 | console.error('CW error', error, error.stack) 60 | webWorker = false 61 | } 62 | tmpWorker.postMessage('b') 63 | webWorker = tmpWorker 64 | } 65 | 66 | const performTaskWorker = (task, params, embed) => { 67 | // console.log(rework_d_T(), 'CW start', task) 68 | const deferred = blueDefer() 69 | 70 | awaiting[taskID] = deferred 71 | 72 | params.task = task 73 | params.taskID = taskID 74 | ;(embed || webWorker).postMessage(params) 75 | 76 | taskID++ 77 | 78 | return deferred.promise 79 | } 80 | 81 | const sha1Hash = bytes => { 82 | if (useSha1Crypto) { 83 | // We don't use buffer since typedArray.subarray(...).buffer gives the whole buffer and not sliced one. 84 | // webCrypto.digest supports typed array 85 | const bytesTyped = convertIfArray(bytes) 86 | // console.log(rework_d_T(), 'Native sha1 start') 87 | return webCrypto.digest({ name: 'SHA-1' }, bytesTyped).then(digest => 88 | // console.log(rework_d_T(), 'Native sha1 done') 89 | digest, e => { 90 | console.error('Crypto digest error', e) 91 | useSha1Crypto = false 92 | return sha1HashSync(bytes) 93 | }) 94 | } 95 | return smartTimeout.immediate(sha1HashSync, bytes) 96 | } 97 | 98 | const sha256Hash = bytes => { 99 | if (useSha256Crypto) { 100 | const bytesTyped = convertIfArray(bytes) 101 | // console.log(rework_d_T(), 'Native sha1 start') 102 | return webCrypto.digest({ name: 'SHA-256' }, bytesTyped) 103 | .then(identity 104 | // console.log(rework_d_T(), 'Native sha1 done') 105 | , e => { 106 | console.error('Crypto digest error', e) 107 | useSha256Crypto = false 108 | return sha256HashSync(bytes) 109 | }) 110 | } 111 | return smartTimeout.immediate(sha256HashSync, bytes) 112 | } 113 | 114 | const aesEncrypt = (bytes, keyBytes, ivBytes) => 115 | smartTimeout.immediate(() => convertToArrayBuffer(aesEncryptSync(bytes, keyBytes, ivBytes))) 116 | 117 | const aesDecrypt = (encryptedBytes, keyBytes, ivBytes) => 118 | smartTimeout.immediate(() => convertToArrayBuffer( 119 | aesDecryptSync(encryptedBytes, keyBytes, ivBytes))) 120 | 121 | const factorize = bytes => { 122 | bytes = convertToByteArray(bytes) 123 | return webWorker 124 | ? performTaskWorker('factorize', { bytes }) 125 | : smartTimeout.immediate(pqPrimeFactorization, bytes) 126 | } 127 | 128 | const modPow = (x, y, m) => webWorker 129 | ? performTaskWorker('mod-pow', { 130 | x, 131 | y, 132 | m 133 | }) 134 | : smartTimeout.immediate(bytesModPow, x, y, m) 135 | 136 | export const CryptoWorker = { 137 | sha1Hash, 138 | sha256Hash, 139 | aesEncrypt, 140 | aesDecrypt, 141 | factorize, 142 | modPow 143 | } 144 | 145 | export default CryptoWorker -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ‼️ **Better use [@mtproto/core](https://github.com/alik0211/mtproto-core)** 2 | 3 | # telegram-mtproto 4 | 5 | [![npm version][npm-image]][npm-url] 6 | 7 | **Telegram Mobile Protocol** [(MTProto)](https://core.telegram.org/mtproto) library in **es6** 8 | 9 | ## About MTProto.. 10 | 11 | **MTProto** is the [Telegram Messenger](http://www.telegram.org) protocol 12 | _"designed for access to a server API from applications running on mobile devices"_. 13 | 14 | The Mobile Protocol is subdivided into three components ([from the official site](https://core.telegram.org/mtproto#general-description)): 15 | 16 | - High-level component (API query language): defines the method whereby API 17 | queries and responses are converted to binary messages. 18 | 19 | - Cryptographic (authorization) layer: defines the method by which messages 20 | are encrypted prior to being transmitted through the transport protocol. 21 | 22 | - Transport component: defines the method for the client and the server to transmit 23 | messages over some other existing network protocol (such as, http, https, tcp, udp). 24 | 25 | 26 | 27 | ## telegram-mtproto in short.. 28 | 29 | No more additional libs. 30 | The **telegram-mtproto** library implements the **Mobile Protocol** and provides all features for work with telegram protocol: 31 | 32 | - A high level api for server connection 33 | 34 | - Promise-based API 35 | 36 | - **HTTP connections** implemented in the transport layer 37 | 38 | - **Web worker** support for blazing fast crypto math works in background 39 | 40 | - A cipher implementation for **AES and RSA encryption** in the security layer 41 | 42 | - Both **plain-text and encrypted message** to communicate data with the server 43 | 44 | - **Diffie-Hellman key exchange** supported by the **prime factorization** function implemented in the security layer 45 | 46 | - **MTProto TL-Schema** compilation as **javascript classes and functions** 47 | 48 | - Custom **async storage** support for saving user data between sessions 49 | 50 | 51 | ## Installation 52 | 53 | ```bash 54 | $ npm install --save telegram-mtproto@beta 55 | ``` 56 | 57 | ## Usage 58 | 59 | ```javascript 60 | import MTProto from 'telegram-mtproto' 61 | 62 | const phone = { 63 | num : '+9996620001', 64 | code: '22222' 65 | } 66 | 67 | const api = { 68 | layer : 57, 69 | initConnection : 0x69796de9, 70 | api_id : 49631 71 | } 72 | 73 | const server = { 74 | dev: true //We will connect to the test server. 75 | } //Any empty configurations fields can just not be specified 76 | 77 | const client = MTProto({ server, api }) 78 | 79 | async function connect(){ 80 | const { phone_code_hash } = await client('auth.sendCode', { 81 | phone_number : phone.num, 82 | current_number: false, 83 | api_id : 49631, 84 | api_hash : 'fb050b8f6771e15bfda5df2409931569' 85 | }) 86 | const { user } = await client('auth.signIn', { 87 | phone_number : phone.num, 88 | phone_code_hash: phone_code_hash, 89 | phone_code : phone.code 90 | }) 91 | 92 | console.log('signed as ', user) 93 | } 94 | 95 | connect() 96 | ``` 97 | 98 | Above we used two functions from the API. 99 | ```typescript 100 | type auth.sendCode = (phone_number: string, sms_type: int, 101 | api_id: int, api_hash: string, lang_code: string) => { 102 | phone_registered: boolean, 103 | phone_code_hash: string, 104 | send_call_timeout: int, 105 | is_password: boolean 106 | } 107 | 108 | type auth.signIn = (phone_number: string, phone_code_hash: string, phone_code: string) => { 109 | expires: int, 110 | user: User 111 | } 112 | ``` 113 | [More][send-code] about [them][sign-in], as well as about many other methods, you can read in the [official documentation][docs]. 114 | 115 | Additional examples can be obtained from [examples][examples] folder. 116 | 117 | ## Storage 118 | 119 | You can use your own storages like [localForage][localForage] for saving data. 120 | Module accepts the following interface 121 | 122 | ```typescript 123 | interface AsyncStorage { 124 | get(key: string): Promise; 125 | set(key: string, value: any): Promise; 126 | remove(...keys: string[]): Promise; 127 | clear(): Promise; 128 | } 129 | ``` 130 | 131 | ```javascript 132 | import { MTProto } from 'telegram-mtproto' 133 | import { api } from './config' 134 | import CustomStorage from './storage' 135 | 136 | const client = MTProto({ 137 | api, 138 | app: { 139 | storage: CustomStorage 140 | } 141 | }) 142 | 143 | ``` 144 | 145 | ## License 146 | 147 | The project is released under the [Mit License](./LICENSE) 148 | 149 | [examples]: https://github.com/zerobias/telegram-mtproto/tree/develop/examples 150 | [localForage]: https://github.com/localForage/localForage 151 | [docs]: https://core.telegram.org/ 152 | [send-code]: https://core.telegram.org/method/auth.sendCode 153 | [sign-in]: https://core.telegram.org/method/auth.signIn 154 | [npm-url]: https://www.npmjs.org/package/telegram-mtproto 155 | [npm-image]: https://badge.fury.io/js/telegram-mtproto.svg 156 | -------------------------------------------------------------------------------- /src/vendor/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 8, 5 | "ecmaFeatures": { 6 | "experimentalObjectRestSpread": true 7 | } 8 | }, 9 | "extends": [ 10 | "eslint:recommended" 11 | ], 12 | "plugins": [ 13 | "babel", 14 | "promise" 15 | ], 16 | "globals": { 17 | "inject": true, 18 | "setZeroTimeout": true 19 | }, 20 | "env": { 21 | "node" : true, 22 | "browser": false, 23 | "worker": true, 24 | "es6" : true, 25 | "jasmine": false, 26 | "jquery": false 27 | }, 28 | "rules": { 29 | // "strict" : "error", 30 | "no-console" : "off", 31 | "no-cond-assign" : "warn", 32 | 33 | //Controversial rules 34 | "no-unused-vars" : "warn", 35 | "prefer-template" : "warn", 36 | "no-nested-ternary" : "warn", 37 | "no-unneeded-ternary" : "warn", 38 | "no-underscore-dangle" : "off", 39 | "prefer-const" : "warn", 40 | 41 | //On test 42 | "no-extra-bind" : "error", 43 | "no-case-declarations" : "error", 44 | "no-useless-call" : "error", 45 | "prefer-arrow-callback": [ 46 | "error", 47 | { "allowNamedFunctions": true } 48 | ], 49 | "object-shorthand" : [ 50 | "error", 51 | "always", 52 | { 53 | "avoidQuotes": true, 54 | "ignoreConstructors": true, 55 | "avoidExplicitReturnArrows": true 56 | } 57 | ], 58 | "no-extra-parens" : "off", 59 | "multiline-ternary" : "off", 60 | 61 | //General 62 | "arrow-body-style" : ["error", "as-needed"], 63 | "no-var" : "off", 64 | "no-duplicate-imports" : "error", 65 | 66 | //Style rules. Freely varies according to projects 67 | "quotes" : ["error", "single",{ 68 | "allowTemplateLiterals": true, 69 | "avoidEscape": true 70 | }], 71 | "max-len" : ["error", { 72 | "code": 150, 73 | "tabWidth":2, 74 | "comments":220 //Separated max length for comments 75 | }], 76 | 77 | //Indents,whitespace settings 78 | "key-spacing" : ["warn", { 79 | "singleLine":{ 80 | "beforeColon": false, 81 | "afterColon": true 82 | }, 83 | "multiLine": { 84 | "align": { 85 | "beforeColon": false, 86 | "afterColon": true, 87 | "on": "colon", 88 | "mode": "strict" 89 | } 90 | } 91 | }], 92 | "comma-spacing" : ["error", { 93 | "before": false, 94 | "after": true 95 | }], 96 | "block-spacing" : [ 97 | "error", "always" 98 | ], 99 | "object-curly-spacing" : [2, "always"], 100 | "semi-spacing" : ["error", { 101 | "before": false, 102 | "after": true 103 | }], 104 | "indent" : [1,2,{ 105 | "VariableDeclarator" : { 106 | "var" : 2, 107 | "let" : 2, 108 | "const" : 3 109 | }, 110 | "FunctionDeclaration": { 111 | "parameters": "first" 112 | }, 113 | "FunctionExpression": { 114 | "parameters": "first" 115 | }, 116 | "MemberExpression": 1, 117 | "SwitchCase": 1, 118 | "CallExpression": { 119 | "arguments": "first" 120 | }, 121 | "ArrayExpression": 1, 122 | "ObjectExpression": "first" 123 | }], 124 | "operator-linebreak": ["error", "after", { 125 | "overrides": { 126 | "?": "before", 127 | ":": "before" 128 | } 129 | }], 130 | "newline-per-chained-call": ["error", { 131 | "ignoreChainWithDepth": 2 132 | }], 133 | "keyword-spacing": ["error", { "before": true }], 134 | "space-unary-ops": ["error", { 135 | "words": true, 136 | "nonwords": false 137 | }], 138 | "no-whitespace-before-property": "error", 139 | "generator-star-spacing": ["error", { 140 | "before": false, 141 | "after": true 142 | }], 143 | "arrow-spacing": ["error", { 144 | "before": true, 145 | "after": true 146 | }], 147 | "space-before-function-paren": ["error", "never"], 148 | "rest-spread-spacing" : ["error", "never"], 149 | 150 | //Strict best practices. No reason not to use this 151 | "no-alert" : "error", 152 | "no-bitwise" : 0, 153 | "no-caller" : "error", 154 | "no-global-assign" : "error", 155 | "no-eval" : "error", 156 | "no-implied-eval" : "error", 157 | "no-proto" : "error", 158 | "no-iterator" : "error", 159 | "no-lone-blocks" : "error", 160 | "no-self-compare" : "error", 161 | "no-self-assign" : ["error", { 162 | "props": true 163 | }], 164 | "no-invalid-regexp" : ["error", { 165 | "allowConstructorFlags": ["u", "y"] 166 | }], 167 | "no-with" : "error", 168 | "no-new-func" : "error", 169 | "prefer-rest-params" : "error", 170 | "prefer-spread" : "error", 171 | //Unnecessary semicolon is an annoying visual clutter 172 | "no-extra-semi" : "off", 173 | "semi" : ["error", "always"] 174 | } 175 | } 176 | 177 | // https://github.com/zerobias -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 8, 5 | "ecmaFeatures": { 6 | "experimentalObjectRestSpread": true 7 | } 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:flowtype/recommended" 12 | ], 13 | "plugins": [ 14 | "babel", 15 | "promise", 16 | "flowtype" 17 | ], 18 | "settings": { 19 | "flowtype": { 20 | "onlyFilesWithFlowAnnotation": true 21 | } 22 | }, 23 | "globals": { 24 | "inject": true, 25 | "setZeroTimeout": true 26 | }, 27 | "env": { 28 | "node" : true, 29 | "browser": false, 30 | "worker": true, 31 | "es6" : true, 32 | "jasmine": false, 33 | "jquery": false 34 | }, 35 | "rules": { 36 | // "strict" : "error", 37 | "no-console" : "off", 38 | "no-cond-assign" : "warn", 39 | 40 | //Controversial rules 41 | "no-unused-vars" : "warn", 42 | "prefer-template" : "warn", 43 | "no-nested-ternary" : "warn", 44 | "no-unneeded-ternary" : "warn", 45 | "no-underscore-dangle" : "warn", 46 | "prefer-const" : "warn", 47 | 48 | //On test 49 | "no-extra-bind" : "error", 50 | "no-case-declarations" : "error", 51 | "no-useless-call" : "error", 52 | "prefer-arrow-callback": [ 53 | "error", 54 | { "allowNamedFunctions": true } 55 | ], 56 | "object-shorthand" : [ 57 | "error", 58 | "always", 59 | { 60 | "avoidQuotes": true, 61 | "ignoreConstructors": true, 62 | "avoidExplicitReturnArrows": true 63 | } 64 | ], 65 | "no-extra-parens" : "error", 66 | "multiline-ternary" : "off", 67 | 68 | //General 69 | "arrow-body-style" : ["error", "as-needed"], 70 | "no-var" : "error", 71 | "no-duplicate-imports" : "off", 72 | 73 | //Style rules. Freely varies according to projects 74 | "quotes" : ["error", "single",{ 75 | "allowTemplateLiterals": true, 76 | "avoidEscape": true 77 | }], 78 | "max-len" : ["error", { 79 | "code": 120, 80 | "tabWidth":2, 81 | "comments":220 //Separated max length for comments 82 | }], 83 | 84 | //Indents,whitespace settings 85 | "key-spacing" : ["warn", { 86 | "singleLine":{ 87 | "beforeColon": false, 88 | "afterColon": true 89 | }, 90 | "multiLine": { 91 | "align": { 92 | "beforeColon": false, 93 | "afterColon": true, 94 | "on": "colon", 95 | "mode": "strict" 96 | } 97 | } 98 | }], 99 | "comma-spacing" : ["error", { 100 | "before": false, 101 | "after": true 102 | }], 103 | "block-spacing" : [ 104 | "error", "always" 105 | ], 106 | "object-curly-spacing" : [2, "always"], 107 | "semi-spacing" : ["error", { 108 | "before": false, 109 | "after": true 110 | }], 111 | "indent" : [1,2,{ 112 | "VariableDeclarator" : { 113 | "var" : 2, 114 | "let" : 2, 115 | "const" : 3 116 | }, 117 | "FunctionDeclaration": { 118 | "parameters": "first" 119 | }, 120 | "FunctionExpression": { 121 | "parameters": "first" 122 | }, 123 | "MemberExpression": 1, 124 | "SwitchCase": 1, 125 | "CallExpression": { 126 | "arguments": "first" 127 | }, 128 | "ArrayExpression": 1, 129 | "ObjectExpression": "first" 130 | }], 131 | "operator-linebreak": ["error", "after", { 132 | "overrides": { 133 | "?": "before", 134 | ":": "before" 135 | } 136 | }], 137 | "newline-per-chained-call": ["error", { 138 | "ignoreChainWithDepth": 2 139 | }], 140 | "keyword-spacing": ["error", { "before": true }], 141 | "space-unary-ops": ["error", { 142 | "words": true, 143 | "nonwords": false 144 | }], 145 | "no-whitespace-before-property": "error", 146 | "generator-star-spacing": ["error", { 147 | "before": false, 148 | "after": true 149 | }], 150 | "arrow-spacing": ["error", { 151 | "before": true, 152 | "after": true 153 | }], 154 | "space-before-function-paren": ["error", "never"], 155 | "rest-spread-spacing" : ["error", "never"], 156 | 157 | //Strict best practices. No reason not to use this 158 | "no-alert" : "error", 159 | "no-bitwise" : 0, 160 | "no-caller" : "error", 161 | "no-global-assign" : "error", 162 | "no-eval" : "error", 163 | "no-implied-eval" : "error", 164 | "no-proto" : "error", 165 | "no-iterator" : "error", 166 | "no-lone-blocks" : "error", 167 | "no-self-compare" : "error", 168 | "no-self-assign" : ["error", { 169 | "props": true 170 | }], 171 | "no-invalid-regexp" : ["error", { 172 | "allowConstructorFlags": ["u", "y"] 173 | }], 174 | "no-with" : "error", 175 | "no-new-func" : "error", 176 | "prefer-rest-params" : "error", 177 | "prefer-spread" : "error", 178 | //Unnecessary semicolon is an annoying visual clutter 179 | "no-extra-semi" : "error", 180 | "semi" : ["error", "never"] 181 | } 182 | } 183 | 184 | // https://github.com/zerobias -------------------------------------------------------------------------------- /src/tl/mediator.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import { TypeWriter, TypeBuffer } from './type-buffer' 4 | import { longToInts, stringToChars, lshift32, bytesToHex } from '../bin' 5 | 6 | import Logger from '../util/log' 7 | const log = Logger`tl:mediator` 8 | 9 | import type { BinaryData } from './index.h' 10 | 11 | export const WriteMediator = { 12 | int(ctx: TypeWriter, i: number, field: string = '') { 13 | ctx.writeInt(i, `${ field }:int`) 14 | }, 15 | bool(ctx: TypeWriter, i: boolean, field: string = '') { 16 | if (i) { 17 | ctx.writeInt(0x997275b5, `${ field }:bool`) 18 | } else { 19 | ctx.writeInt(0xbc799737, `${ field }:bool`) 20 | } 21 | }, 22 | longP(ctx: TypeWriter, 23 | iHigh: number, 24 | iLow: number, 25 | field: string) { 26 | ctx.writePair(iLow, iHigh, 27 | `${ field }:long[low]`, 28 | `${ field }:long[high]`) 29 | }, 30 | long(ctx: TypeWriter, 31 | sLong?: number[] | string | number, 32 | field: string = '') { 33 | if (Array.isArray(sLong)) 34 | return sLong.length === 2 35 | ? this.longP(ctx, sLong[0], sLong[1], field) 36 | : this.intBytes(ctx, sLong, 64, field) 37 | let str 38 | if (typeof sLong !== 'string') 39 | str = sLong 40 | ? sLong.toString() 41 | : '0' 42 | else str = sLong 43 | const [int1, int2] = longToInts(str) 44 | ctx.writePair(int2, int1, 45 | `${ field }:long[low]`, 46 | `${ field }:long[high]`) 47 | }, 48 | double(ctx: TypeWriter, f: number, field: string = '') { 49 | const buffer = new ArrayBuffer(8) 50 | const intView = new Int32Array(buffer) 51 | const doubleView = new Float64Array(buffer) 52 | 53 | doubleView[0] = f 54 | 55 | const [int1, int2] = intView 56 | ctx.writePair(int2, int1, 57 | `${ field }:double[low]`, 58 | `${ field }:double[high]`) 59 | }, 60 | bytes(ctx: TypeWriter, 61 | bytes?: number[] | ArrayBuffer | string, 62 | field: string = '') { 63 | const { list, length } = binaryDataGuard(bytes) 64 | // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:bytes`) 65 | 66 | ctx.checkLength(length + 8) 67 | if (length <= 253) { 68 | ctx.next(length) 69 | } else { 70 | ctx.next(254) 71 | ctx.next(length & 0xFF) 72 | ctx.next((length & 0xFF00) >> 8) 73 | ctx.next((length & 0xFF0000) >> 16) 74 | } 75 | 76 | ctx.set(list, length) 77 | ctx.addPadding() 78 | }, 79 | intBytes(ctx: TypeWriter, 80 | bytes: BinaryData | ArrayBuffer | string, 81 | bits: number | false, 82 | field: string = '') { 83 | const { list, length } = binaryDataGuard(bytes) 84 | 85 | if (bits) { 86 | if (bits % 32 || length * 8 != bits) { 87 | throw new Error(`Invalid bits: ${ bits }, ${length}`) 88 | } 89 | } 90 | // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:int${ bits}`) 91 | ctx.checkLength(length) 92 | ctx.set(list, length) 93 | } 94 | } 95 | 96 | export const ReadMediator = { 97 | int(ctx: TypeBuffer, field: string) { 98 | const result = ctx.nextInt() 99 | log('read, int')(field, result) 100 | return result 101 | }, 102 | long(ctx: TypeBuffer, field: string ){ 103 | const iLow = this.int(ctx, `${ field }:long[low]`) 104 | const iHigh = this.int(ctx, `${ field }:long[high]`) 105 | 106 | const res = lshift32(iHigh, iLow) 107 | return res 108 | }, 109 | double(ctx: TypeBuffer, field: string) { 110 | const buffer = new ArrayBuffer(8) 111 | const intView = new Int32Array(buffer) 112 | const doubleView = new Float64Array(buffer) 113 | 114 | intView[0] = this.int(ctx, `${ field }:double[low]`) 115 | intView[1] = this.int(ctx, `${ field }:double[high]`) 116 | 117 | return doubleView[0] 118 | }, 119 | string(ctx: TypeBuffer, field: string) { 120 | const bytes = this.bytes(ctx, `${field}:string`) 121 | const sUTF8 = [...bytes] 122 | .map(getChar) 123 | .join('') 124 | 125 | let s 126 | try { 127 | s = decodeURIComponent(escape(sUTF8)) 128 | } catch (e) { 129 | s = sUTF8 130 | } 131 | 132 | log(`read, string`)(s, `${field}:string`) 133 | 134 | return s 135 | }, 136 | bytes(ctx: TypeBuffer, field: string) { 137 | let len = ctx.nextByte() 138 | 139 | if (len == 254) { 140 | len = ctx.nextByte() | 141 | ctx.nextByte() << 8 | 142 | ctx.nextByte() << 16 143 | } 144 | 145 | const bytes = ctx.next(len) 146 | ctx.addPadding() 147 | 148 | log(`read, bytes`)(bytesToHex(bytes), `${ field }:bytes`) 149 | 150 | return bytes 151 | } 152 | } 153 | 154 | const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) => { 155 | let list, length 156 | if (bytes instanceof ArrayBuffer) { 157 | list = new Uint8Array(bytes) 158 | length = bytes.byteLength 159 | } else if (typeof bytes === 'string') { 160 | list = 161 | stringToChars( 162 | unescape( 163 | encodeURIComponent( 164 | bytes))) 165 | length = list.length 166 | } else if (bytes === undefined) { 167 | list = [] 168 | length = 0 169 | } else { 170 | list = bytes 171 | length = bytes.length 172 | } 173 | return { 174 | list, 175 | length 176 | } 177 | } 178 | 179 | const getChar = (e: number) => String.fromCharCode(e) 180 | -------------------------------------------------------------------------------- /logs/async fail404.log: -------------------------------------------------------------------------------- 1 | [0.370] mtpAuth 2 | [0.409] Send req_pq bf891aafebc4e31d38d7740f5e931524 3 | [0.761] Got ResPQ 564f9b1ca805ce1bb3d823c3e11afee1 13efb429daeff551 [ '14101943622620965665' ] 4 | [0.778] PQ factorization start Uint8Array [ 19, 239, 180, 41, 218, 239, 245, 81 ] 5 | Get networker error { Error: MT[404] REQUEST_FAILED: https://venus.web.telegram.org/apiw_test1 6 | at onlySendPlainErr (C:\_projects\Web\mtproto2\source\service\authorizer\send-plain-req.js:47:17) 7 | From previous event: 8 | at mtpSendReqDhParams (C:\_projects\Web\mtproto2\source\service\authorizer\index.js:165:12) 9 | 10 | err undefined 11 | [2.751] PQ factorization done 45485 12 | [2.764] Send req_DH_params 13 | [3.001] mtpAuth 14 | [3.011] Send req_pq e106792300c3abe6ef4ee51eefd90671 15 | [3.199] Got ResPQ 92571ffa85eb53751520e4c39f40c379 1dfd2e3df63b1b97 [ '14101943622620965665' ] 16 | [3.208] PQ factorization start Uint8Array [ 29, 253, 46, 61, 246, 59, 27, 151 ] 17 | 18 | Get networker error { Error: MT[404] REQUEST_FAILED: https://venus.web.telegram.org/apiw_test1 19 | at onlySendPlainErr (C:\_projects\Web\mtproto2\source\service\authorizer\send-plain-req.js:47:17) 20 | err undefined 21 | 22 | [4.646] PQ factorization done 28896 23 | [4.650] Send req_DH_params 24 | [4.893] mtpAuth 25 | [4.896] Send req_pq ace1584e1b027695904863fa2a10a148 26 | [5.082] Got ResPQ 879b12a6c78145dcf8a9c83923e65ccb 229a6669808c111b [ '14101943622620965665' ] 27 | [5.090] PQ factorization start Uint8Array [ 34, 154, 102, 105, 128, 140, 17, 27 ] 28 | [6.163] Apply server time 1487876401 1487876392351 9 false 29 | [5.917] PQ factorization done 22008 30 | [5.932] Send req_DH_params 31 | [6.156] Done decrypting answer 32 | [6.156] Verifying DH params 33 | [6.157] dhPrime cmp OK 34 | [6.162] 2^{2048-64} < gA < dhPrime-2^{2048-64} OK 35 | [6.666] Send set_client_DH_params 36 | [7.561] Api call help.getNearestDc {} 6390380488649317148 1 { dcID: 2, createNetworker: true, resultType: 'NearestDc' } 37 | [7.567] Container [ '6390380488649317148', '6390380488663633540' ] 6390380488663633544 4 38 | [7.540] Got Set_client_DH_params_answer dh_gen_ok 39 | (node:20412) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\source\service\networker.js:520:14 but was not returned from it, see http://goo.gl/rR 40 | qMUw 41 | at Function.module.exports.Promise.cast (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:195:13) 42 | [8.004] MT call http_wait { max_delay: 500, wait_after: 150, max_wait: 25000 } 6390380491777917380 5 43 | [8.047] Server response 2 { response: { _: 'msg_container', messages: [ [Object], [Object] ] }, 44 | messageID: '6390380493821942785', 45 | sessionID: [ 223, 30, 67, 159, 81, 217, 158, 87 ], 46 | seqNo: 4 } 47 | [8.060] Rpc response { _: 'nearestDc', country: 'RU', this_dc: 2, nearest_dc: 2 } 48 | getNearestDc { _: 'nearestDc', country: 'RU', this_dc: 2, nearest_dc: 2 } 49 | [8.070] Api call auth.sendCode { phone_number: '+9996620000', 50 | current_number: false, 51 | api_id: 49631, 52 | api_hash: 'fb050b8f6771e15bfda5df2409931569' } 6390380491916267308 7 { resultType: 'auth.SentCode' } 53 | [8.079] MT message { _: 'msgs_ack', 54 | msg_ids: [ '6390380492935428097', '6390380493174051841' ] } 6390380491935409948 8 55 | [8.083] Container [ '6390380491916267308', 56 | '6390380491935409948', 57 | '6390380491943530548' ] 6390380491943775932 10 58 | (node:20412) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\source\service\networker.js:523:12 but was not returned from it, see http://goo.gl/rR 59 | qMUw 60 | at Function.module.exports.Promise.cast (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:195:13) 61 | [8.478] Server response 2 { response: 62 | { _: 'rpc_result', 63 | req_msg_id: '6390380491916267308', 64 | result: 65 | { _: 'auth.sentCode', 66 | pFlags: [Object], 67 | flags: 3, 68 | type: [Object], 69 | phone_code_hash: '0689926c11808148e0', 70 | next_type: [Object] } }, 71 | messageID: '6390380495231232001', 72 | sessionID: [ 223, 30, 67, 159, 81, 217, 158, 87 ], 73 | seqNo: 5 } 74 | [8.483] Rpc response { _: 'auth.sentCode', 75 | pFlags: { phone_registered: true }, 76 | flags: 3, 77 | type: { _: 'auth.sentCodeTypeApp', length: 5 }, 78 | phone_code_hash: '0689926c11808148e0', 79 | next_type: { _: 'auth.codeTypeSms' } } 80 | phone_code_hash 0689926c11808148e0 81 | [8.501] Api call auth.signIn { phone_number: '+9996620000', 82 | phone_code_hash: '0689926c11808148e0', 83 | phone_code: '22222' } 6390380492820404228 11 { resultType: 'auth.Authorization' } 84 | [8.509] MT message { _: 'msgs_ack', msg_ids: [ '6390380495231232001' ] } 6390380492837028668 12 85 | [8.516] Container [ '6390380492820404228', 86 | '6390380492837028668', 87 | '6390380492849640988' ] 6390380492851751444 14 88 | (node:20412) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\source\service\networker.js:523:12 but was not returned from it, see http://goo.gl/rR 89 | qMUw 90 | [8.922] Server response 2 { response: 91 | { _: 'rpc_result', 92 | req_msg_id: '6390380492820404228', 93 | result: { _: 'auth.authorization', pFlags: {}, flags: 0, user: [Object] } }, 94 | messageID: '6390380497133619201', 95 | sessionID: [ 223, 30, 67, 159, 81, 217, 158, 87 ], 96 | seqNo: 7 } 97 | [8.932] Rpc response { _: 'auth.authorization', 98 | pFlags: {}, 99 | flags: 0, 100 | user: 101 | { _: 'user', 102 | pFlags: { self: true }, 103 | flags: 1111, 104 | id: 297169, 105 | access_hash: '6362957075171533052', 106 | first_name: 'Qwe', 107 | last_name: 'Rty', 108 | phone: '9996620000', 109 | status: { _: 'userStatusOnline', expires: 1487876464 } } } -------------------------------------------------------------------------------- /src/tl/type-buffer.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import isNode from 'detect-node' 4 | 5 | import Logger from '../util/log' 6 | const log = Logger('tl', 'type-buffer') 7 | 8 | import { immediate } from '../util/smart-timeout' 9 | import { TypeBufferIntError } from '../error' 10 | 11 | // import { bigint, uintToInt, intToUint, bytesToHex, 12 | // gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' 13 | 14 | import type { BinaryData, TLConstruct, TLSchema } from './index.h' 15 | 16 | function findType(val: TLConstruct) { 17 | return val.type == this 18 | } 19 | 20 | function findPred(val: TLConstruct) { 21 | return val.predicate == this 22 | } 23 | 24 | function findId(val: TLConstruct) { 25 | return val.id == this 26 | } 27 | 28 | export const getNakedType = (type: string, schema: TLSchema) => { 29 | const checkType = type.substr(1) 30 | const result = schema.constructors.find(findType, checkType) 31 | if (!result) 32 | throw new Error(`Constructor not found for type: ${type}`) 33 | return result 34 | } 35 | 36 | export const getPredicate = (type: string, schema: TLSchema) => { 37 | const result = schema.constructors.find(findPred, type) 38 | if (!result) 39 | throw new Error(`Constructor not found for predicate: ${type}`) 40 | return result 41 | } 42 | 43 | export const getTypeConstruct = 44 | (construct: number, schema: TLSchema) => 45 | schema.constructors.find(findId, construct) 46 | 47 | const getChar = (e: number) => String.fromCharCode(e) 48 | 49 | export const getString = (length: number, buffer: TypeBuffer) => { 50 | const bytes = buffer.next(length) 51 | 52 | const result = [...bytes].map(getChar).join('') 53 | buffer.addPadding() 54 | return result 55 | } 56 | 57 | const countNewLength = (maxLength: number, need: number, offset: number) => { 58 | const e1 = maxLength * 2 59 | const e2 = offset + need + 16 60 | const max = Math.max(e1, e2) / 4 61 | const rounded = Math.ceil(max) * 4 62 | return rounded 63 | } 64 | 65 | const writeIntLogger = log('writeInt') 66 | 67 | const writeIntLog = (i: number, field: string) => { 68 | const hex = i && i.toString(16) || 'UNDEF' 69 | writeIntLogger(hex, i, field) 70 | } 71 | 72 | export class TypeWriter { 73 | offset: number = 0 // in bytes 74 | buffer: ArrayBuffer 75 | intView: Int32Array 76 | byteView: Uint8Array 77 | maxLength: number 78 | constructor(/*startMaxLength: number*/) { 79 | // this.maxLength = startMaxLength 80 | // this.reset() 81 | } 82 | reset() { 83 | this.buffer = new ArrayBuffer(this.maxLength) 84 | this.intView = new Int32Array(this.buffer) 85 | this.byteView = new Uint8Array(this.buffer) 86 | } 87 | set(list: BinaryData, length: number) { 88 | this.byteView.set(list, this.offset) 89 | this.offset += length 90 | } 91 | next(data: number) { 92 | this.byteView[this.offset] = data 93 | this.offset++ 94 | } 95 | checkLength(needBytes: number) { 96 | if (this.offset + needBytes < this.maxLength) { 97 | return 98 | } 99 | log('Increase buffer')(this.offset, needBytes, this.maxLength) 100 | this.maxLength = countNewLength( 101 | this.maxLength, 102 | needBytes, 103 | this.offset 104 | ) 105 | const previousBuffer = this.buffer 106 | const previousArray = new Int32Array(previousBuffer) 107 | 108 | this.reset() 109 | 110 | new Int32Array(this.buffer).set(previousArray) 111 | } 112 | getArray() { 113 | const resultBuffer = new ArrayBuffer(this.offset) 114 | const resultArray = new Int32Array(resultBuffer) 115 | 116 | resultArray.set(this.intView.subarray(0, this.offset / 4)) 117 | 118 | return resultArray 119 | } 120 | getBuffer() { 121 | return this.getArray().buffer 122 | } 123 | getBytesTyped() { 124 | const resultBuffer = new ArrayBuffer(this.offset) 125 | const resultArray = new Uint8Array(resultBuffer) 126 | 127 | resultArray.set(this.byteView.subarray(0, this.offset)) 128 | 129 | return resultArray 130 | } 131 | getBytesPlain() { 132 | const bytes = [] 133 | for (let i = 0; i < this.offset; i++) { 134 | bytes.push(this.byteView[i]) 135 | } 136 | return bytes 137 | } 138 | writeInt(i: number, field: string) { 139 | immediate(writeIntLog, i, field) 140 | 141 | this.checkLength(4) 142 | this.intView[this.offset / 4] = i 143 | this.offset += 4 144 | } 145 | writePair(n1: number, n2: number, field1: string, field2: string) { 146 | this.writeInt(n1, field1) 147 | this.writeInt(n2, field2) 148 | } 149 | addPadding() { 150 | while (this.offset % 4) 151 | this.next(0) 152 | } 153 | } 154 | 155 | export class TypeBuffer { 156 | offset: number = 0 157 | buffer: Buffer 158 | intView: Uint32Array 159 | byteView: Uint8Array 160 | constructor(buffer: Buffer) { 161 | this.buffer = buffer 162 | this.intView = toUint32(buffer) 163 | this.byteView = new Uint8Array(buffer) 164 | } 165 | 166 | nextByte() { 167 | return this.byteView[this.offset++] 168 | } 169 | nextInt() { 170 | if (this.offset >= this.intView.length * 4) 171 | throw new TypeBufferIntError(this) 172 | const int = this.intView[this.offset / 4] 173 | this.offset += 4 174 | return int 175 | } 176 | readPair(field1: string, field2: string) { 177 | const int1 = this.nextInt(field1) 178 | const int2 = this.nextInt(field2) 179 | return [ int1, int2 ] 180 | } 181 | next(length: number) { 182 | const result = this.byteView.subarray(this.offset, this.offset + length) 183 | this.offset += length 184 | return result 185 | } 186 | isEnd() { 187 | return this.offset === this.byteView.length 188 | } 189 | addPadding() { 190 | const offset = this.offset % 4 191 | if (offset > 0) 192 | this.offset += 4 - offset 193 | } 194 | } 195 | 196 | const toUint32 = (buf: Buffer) => { 197 | let ln, res 198 | if (!isNode) //TODO browser behavior not equals, why? 199 | return new Uint32Array( buf ) 200 | if (buf.readUInt32LE) { 201 | ln = buf.byteLength / 4 202 | res = new Uint32Array( ln ) 203 | for (let i = 0; i < ln; i++) 204 | res[i] = buf.readUInt32LE( i*4 ) 205 | } else { 206 | //$FlowIssue 207 | const data = new DataView( buf ) 208 | ln = data.byteLength / 4 209 | res = new Uint32Array( ln ) 210 | for (let i = 0; i < ln; i++) 211 | res[i] = data.getUint32( i*4, true ) 212 | } 213 | return res 214 | } 215 | -------------------------------------------------------------------------------- /src/layout/index.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import has from 'ramda/src/has' 4 | import flip from 'ramda/src/flip' 5 | import contains from 'ramda/src/contains' 6 | 7 | import type { TLParam, SchemaElement, TLMethod, TLConstruct, TLSchema, SchemaParam } from './index.h' 8 | 9 | class TypeClass { 10 | name: string 11 | 12 | types: Set = new Set 13 | constructor(name: string) { 14 | this.name = name 15 | } 16 | isTypeOf(value: any): boolean { 17 | return false 18 | } 19 | } 20 | 21 | class Argument { 22 | id: string 23 | name: string 24 | typeClass: string 25 | isVector: boolean 26 | isBare: boolean 27 | isFlag: boolean 28 | flagIndex: number 29 | fullType: string 30 | constructor(id: string, 31 | name: string, 32 | typeClass: string, 33 | isVector: boolean = false, 34 | isBare: boolean = false, 35 | isFlag: boolean = false, 36 | flagIndex: number = NaN) { 37 | this.name = name 38 | this.typeClass = typeClass 39 | this.isVector = isVector 40 | this.isBare = isBare 41 | this.isFlag = isFlag 42 | this.flagIndex = flagIndex 43 | this.id = id 44 | 45 | this.fullType = Argument.fullType(this) 46 | } 47 | static fullType(obj: Argument) { 48 | const { typeClass, isVector, isFlag, flagIndex } = obj 49 | let result = typeClass 50 | if (isVector) 51 | result = `Vector<${result}>` 52 | if (isFlag) 53 | result = `flags.${flagIndex}?${result}` 54 | return result 55 | } 56 | } 57 | 58 | class Creator { 59 | id: number 60 | name: string //predicate or method 61 | hasFlags: boolean 62 | params: Argument[] 63 | constructor(id: number, 64 | name: string, 65 | hasFlags: boolean, 66 | params: Argument[]) { 67 | this.id = id 68 | this.name = name 69 | this.hasFlags = hasFlags 70 | this.params = params 71 | } 72 | } 73 | 74 | class Method extends Creator { 75 | returns: string 76 | constructor(id: number, 77 | name: string, 78 | hasFlags: boolean, 79 | params: Argument[], 80 | returns: string) { 81 | super(id, name, hasFlags, params) 82 | this.returns = returns 83 | } 84 | } 85 | 86 | class Type extends Creator { 87 | typeClass: string 88 | constructor(id: number, 89 | name: string, 90 | hasFlags: boolean, 91 | params: Argument[], 92 | typeClass: string) { 93 | super(id, name, hasFlags, params) 94 | this.typeClass = typeClass 95 | } 96 | } 97 | 98 | const isFlagItself = 99 | (param: SchemaParam) => 100 | param.name === 'flags' && 101 | param.type === '#' 102 | 103 | export class Layout { 104 | typeClasses: Map = new Map 105 | creators: Set = new Set 106 | args: Map = new Map 107 | funcs: Map = new Map 108 | types: Map = new Map 109 | typesById: Map = new Map 110 | typeDefaults: Map = new Map 111 | schema: TLSchema 112 | makeCreator(elem: SchemaElement, 113 | name: string, 114 | sign: string, 115 | Construct: typeof Method | typeof Type) { 116 | const args: Argument[] = [] 117 | let hasFlags = false 118 | for (const [ i, param ] of elem.params.entries()) { 119 | if (isFlagItself(param)) { 120 | hasFlags = true 121 | continue 122 | } 123 | const id = `${name}.${param.name}/${i}` 124 | const { typeClass, isVector, isFlag, flagIndex, isBare } = getTypeProps(param.type) 125 | if (isFlag) hasFlags = true 126 | this.pushTypeClass(typeClass) 127 | const arg = new Argument(id, param.name, typeClass, isVector, isBare, isFlag, flagIndex) 128 | args.push(arg) 129 | this.args.set(id, arg) 130 | } 131 | const id = parseInt(elem.id, 10) 132 | const creator = new Construct(id, name, hasFlags, args, sign) 133 | this.creators.add(name) 134 | if (creator instanceof Method) 135 | this.funcs.set(name, creator) 136 | else if (creator instanceof Type) { 137 | this.types.set(name, creator) 138 | this.typesById.set(id, creator) 139 | } 140 | } 141 | makeMethod(elem: TLMethod) { 142 | const name = elem.method 143 | const returns = elem.type 144 | this.pushTypeClass(returns, name) 145 | this.makeCreator(elem, name, returns, Method) 146 | } 147 | pushTypeClass(typeClass: string, type?: string) { 148 | let instance 149 | if (this.typeClasses.has(typeClass)) 150 | instance = this.typeClasses.get(typeClass) 151 | else { 152 | instance = new TypeClass(typeClass) 153 | this.typeClasses.set(typeClass, instance) 154 | } 155 | if (type && instance) 156 | instance.types.add(type) 157 | } 158 | makeType(elem: TLConstruct) { 159 | const name = elem.predicate 160 | const typeClass = elem.type 161 | this.pushTypeClass(typeClass, name) 162 | this.makeCreator(elem, name, typeClass, Type) 163 | } 164 | makeLayout(schema: TLSchema) { 165 | const { methods, constructors } = schema 166 | constructors.map(this.makeType) 167 | methods.map(this.makeMethod) 168 | for (const [ key, type ] of this.types.entries()) 169 | if (hasEmpty(key)) 170 | this.typeDefaults.set(type.typeClass, { _: key }) 171 | } 172 | constructor(schema: TLSchema) { 173 | //$FlowIssue 174 | this.makeType = this.makeType.bind(this) 175 | //$FlowIssue 176 | this.makeMethod = this.makeMethod.bind(this) 177 | // this.schema = schema 178 | this.makeLayout(schema) 179 | } 180 | } 181 | const hasEmpty = contains('Empty') 182 | const hasQuestion = contains('?') 183 | const hasVector = contains('<') 184 | const hasBare = contains('%') 185 | 186 | export const getTypeProps = (rawType: string) => { 187 | const result = { 188 | typeClass: rawType, 189 | isVector : false, 190 | isFlag : false, 191 | flagIndex: NaN, 192 | isBare : false 193 | } 194 | if (hasQuestion(rawType)) { 195 | const [ prefix, rest ] = rawType.split('?') 196 | const [ , index ] = prefix.split('.') 197 | result.isFlag = true 198 | result.flagIndex = +index 199 | result.typeClass = rest 200 | } 201 | if (hasVector(result.typeClass)) { 202 | result.isVector = true 203 | result.typeClass = result.typeClass.slice(7, -1) 204 | } 205 | if (hasBare(result.typeClass)) { 206 | result.isBare = true 207 | result.typeClass = result.typeClass.slice(1) 208 | } 209 | return result 210 | } 211 | 212 | export const isSimpleType: (type: string) => boolean = 213 | flip(contains)( 214 | ['int', /*'long',*/ 'string', /*'double', */'true', /*'bytes'*/]) 215 | 216 | const getFlagsRed = 217 | (data: Object) => 218 | (acc: number, { name, flagIndex }: Argument) => 219 | has(name, data) 220 | ? acc + 2 ** flagIndex 221 | : acc 222 | 223 | export const getFlags = ({ params }: Creator) => { 224 | const flagsParams = 225 | params.filter( 226 | (e: Argument) => e.isFlag) 227 | 228 | return (data: Object) => 229 | flagsParams 230 | .reduce( 231 | getFlagsRed(data), 232 | 0) 233 | } 234 | 235 | export default Layout -------------------------------------------------------------------------------- /src/service/api-manager/index.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Promise from 'bluebird' 4 | // import UpdatesManager from '../updates' 5 | 6 | import isNil from 'ramda/src/isNil' 7 | import is from 'ramda/src/is' 8 | import propEq from 'ramda/src/propEq' 9 | import has from 'ramda/src/has' 10 | import pathSatisfies from 'ramda/src/pathSatisfies' 11 | import complement from 'ramda/src/complement' 12 | 13 | import Logger from '../../util/log' 14 | const debug = Logger`api-manager` 15 | 16 | import Auth from '../authorizer' 17 | import type { Args } from '../authorizer' 18 | 19 | import blueDefer from '../../util/defer' 20 | import { dTime } from '../time-manager' 21 | import { chooseServer } from '../dc-configurator' 22 | 23 | import KeyManager from '../rsa-keys-manger' 24 | import { AuthKeyError } from '../../error' 25 | 26 | import { bytesFromHex, bytesToHex } from '../../bin' 27 | 28 | import type { TLFabric } from '../../tl' 29 | import type { TLSchema } from '../../tl/index.h' 30 | import { switchErrors } from './error-cases' 31 | import { delayedCall } from '../../util/smart-timeout' 32 | 33 | import Request from './request' 34 | 35 | import type { Bytes, PublicKey, LeftOptions, AsyncStorage, Cache } from './index.h' 36 | 37 | import type { ApiConfig, StrictConfig } from '../main/index.h' 38 | 39 | import type { Networker } from '../networker' 40 | 41 | import type { Emit, On } from '../main/index.h' 42 | 43 | const hasPath = pathSatisfies( complement( isNil ) ) 44 | 45 | const baseDcID = 2 46 | 47 | const Ln = (length, obj) => obj && propEq('length', length, obj) 48 | 49 | 50 | 51 | export class ApiManager { 52 | cache: Cache = { 53 | uploader : {}, 54 | downloader: {}, 55 | auth : {}, 56 | servers : {}, 57 | keysParsed: {} 58 | } 59 | apiConfig: ApiConfig 60 | publicKeys: PublicKey[] 61 | storage: AsyncStorage 62 | TL: TLFabric 63 | serverConfig: {} 64 | schema: TLSchema 65 | mtSchema: TLSchema 66 | keyManager: Args 67 | networkFabric: any 68 | updatesManager: any 69 | auth: any 70 | on: On 71 | emit: Emit 72 | chooseServer: (dcID: number, upload?: boolean) => {} 73 | constructor(config: StrictConfig, tls: TLFabric, netFabric: Function, { on, emit }: { on: On, emit: Emit }) { 74 | const { 75 | server, 76 | api, 77 | app: { 78 | storage, 79 | publicKeys 80 | }, 81 | schema, 82 | mtSchema 83 | } = config 84 | this.apiConfig = api 85 | this.publicKeys = publicKeys 86 | this.storage = storage 87 | this.serverConfig = server 88 | this.schema = schema 89 | this.mtSchema = mtSchema 90 | this.chooseServer = chooseServer(this.cache.servers, server) 91 | this.on = on 92 | this.emit = emit 93 | this.TL = tls 94 | this.keyManager = KeyManager(this.TL.Serialization, publicKeys, this.cache.keysParsed) 95 | this.auth = Auth(this.TL, this.keyManager) 96 | this.networkFabric = netFabric(this.chooseServer) 97 | this.mtpInvokeApi = this.mtpInvokeApi.bind(this) 98 | this.mtpGetNetworker = this.mtpGetNetworker.bind(this) 99 | const apiManager = this.mtpInvokeApi 100 | apiManager.setUserAuth = this.setUserAuth 101 | apiManager.on = this.on 102 | apiManager.emit = this.emit 103 | apiManager.storage = storage 104 | 105 | // this.updatesManager = UpdatesManager(apiManager) 106 | // apiManager.updates = this.updatesManager 107 | 108 | return apiManager 109 | } 110 | networkSetter = (dc: number, options: LeftOptions) => 111 | (authKey: Bytes, serverSalt: Bytes): Networker => { 112 | const networker = this.networkFabric(dc, authKey, serverSalt, options) 113 | this.cache.downloader[dc] = networker 114 | return networker 115 | } 116 | mtpGetNetworker = async (dcID: number, options: LeftOptions = {}) => { 117 | // const isUpload = options.fileUpload || options.fileDownload 118 | // const cache = isUpload 119 | // ? this.cache.uploader 120 | // : this.cache.downloader 121 | 122 | const cache = this.cache.downloader 123 | if (!dcID) throw new Error('get Networker without dcID') 124 | 125 | if (has(dcID, cache)) return cache[dcID] 126 | 127 | const akk = `dc${ dcID }_auth_key` 128 | const ssk = `dc${ dcID }_server_salt` 129 | 130 | const dcUrl = this.chooseServer(dcID, false) 131 | 132 | const networkSetter = this.networkSetter(dcID, options) 133 | 134 | const authKeyHex = await this.storage.get(akk) 135 | let serverSaltHex = await this.storage.get(ssk) 136 | 137 | if (cache[dcID]) return cache[dcID] 138 | 139 | if (Ln(512, authKeyHex)) { 140 | if (!serverSaltHex || serverSaltHex.length !== 16) 141 | serverSaltHex = 'AAAAAAAAAAAAAAAA' 142 | const authKey = bytesFromHex(authKeyHex) 143 | const serverSalt = bytesFromHex(serverSaltHex) 144 | 145 | return networkSetter(authKey, serverSalt) 146 | } 147 | 148 | if (!options.createNetworker) 149 | throw new AuthKeyError() 150 | 151 | let auth 152 | try { 153 | auth = await this.auth(dcID, this.cache.auth, dcUrl) 154 | } catch (error) { 155 | return netError(error) 156 | } 157 | 158 | const { authKey, serverSalt } = auth 159 | 160 | await this.storage.set(akk, bytesToHex(authKey)) 161 | await this.storage.set(ssk, bytesToHex(serverSalt)) 162 | 163 | return networkSetter(authKey, serverSalt) 164 | } 165 | async initConnection() { 166 | if (!isAnyNetworker(this)) { 167 | const storedBaseDc = await this.storage.get('dc') 168 | const baseDc = storedBaseDc || baseDcID 169 | const opts = { 170 | dcID : baseDc, 171 | createNetworker: true 172 | } 173 | const networker = await this.mtpGetNetworker(baseDc, opts) 174 | const nearestDc = await networker.wrapApiCall( 175 | 'help.getNearestDc', {}, opts) 176 | const { nearest_dc, this_dc } = nearestDc 177 | await this.storage.set('dc', nearest_dc) 178 | debug(`nearest Dc`)('%O', nearestDc) 179 | if (nearest_dc !== this_dc) await this.mtpGetNetworker(nearest_dc, { createNetworker: true }) 180 | } 181 | } 182 | mtpInvokeApi = async (method: string, params: Object, options: LeftOptions = {}) => { 183 | const deferred = blueDefer() 184 | const rejectPromise = (error: any) => { 185 | let err 186 | if (!error) 187 | err = { type: 'ERROR_EMPTY', input: '' } 188 | else if (!is(Object, error)) 189 | err = { message: error } 190 | else err = error 191 | deferred.reject(err) 192 | 193 | if (!options.noErrorBox) { 194 | //TODO weird code. `error` changed after `.reject`? 195 | 196 | /*err.input = method 197 | 198 | err.stack = 199 | stack || 200 | hasPath(['originalError', 'stack'], error) || 201 | error.stack || 202 | (new Error()).stack*/ 203 | this.emit('error.invoke', error) 204 | } 205 | } 206 | 207 | await this.initConnection() 208 | 209 | const requestThunk = waitTime => delayedCall(req.performRequest, +waitTime * 1e3) 210 | 211 | const dcID = options.dcID 212 | ? options.dcID 213 | : await this.storage.get('dc') 214 | 215 | const networker = await this.mtpGetNetworker(dcID, options) 216 | 217 | const cfg = { 218 | networker, 219 | dc : dcID, 220 | storage : this.storage, 221 | getNetworker: this.mtpGetNetworker, 222 | netOpts : options 223 | } 224 | const req = new Request(cfg, method, params) 225 | 226 | 227 | req.performRequest() 228 | .then( 229 | deferred.resolve, 230 | error => { 231 | const deferResolve = deferred.resolve 232 | const apiSavedNet = () => networker 233 | const apiRecall = networker => { 234 | req.config.networker = networker 235 | return req.performRequest() 236 | } 237 | console.error(dTime(), 'Error', error.code, error.type, baseDcID, dcID) 238 | 239 | return switchErrors(error, options, dcID, baseDcID)( 240 | error, options, dcID, this.emit, rejectPromise, requestThunk, 241 | apiSavedNet, apiRecall, deferResolve, this.mtpInvokeApi, 242 | this.storage) 243 | }) 244 | .catch(rejectPromise) 245 | 246 | return deferred.promise 247 | } 248 | 249 | setUserAuth = (dcID: number, userAuth: any) => { 250 | const fullUserAuth = { dcID, ...userAuth } 251 | this.storage.set({ 252 | dc : dcID, 253 | user_auth: fullUserAuth 254 | }) 255 | this.emit('auth.dc', { dc: dcID, auth: userAuth }) 256 | } 257 | async mtpClearStorage() { 258 | const saveKeys = [] 259 | for (let dcID = 1; dcID <= 5; dcID++) { 260 | saveKeys.push(`dc${ dcID }_auth_key`) 261 | saveKeys.push(`t_dc${ dcID }_auth_key`) 262 | } 263 | this.storage.noPrefix() //TODO Remove noPrefix 264 | 265 | const values = await this.storage.get(...saveKeys) 266 | 267 | await this.storage.clear() 268 | 269 | const restoreObj = {} 270 | saveKeys.forEach((key, i) => { 271 | const value = values[i] 272 | if (value !== false && value !== undefined) 273 | restoreObj[key] = value 274 | }) 275 | this.storage.noPrefix() 276 | 277 | return this.storage.set(restoreObj) //TODO definitely broken 278 | } 279 | } 280 | 281 | const isAnyNetworker = (ctx: ApiManager) => Object.keys(ctx.cache.downloader).length > 0 282 | 283 | const netError = error => { 284 | console.log('Get networker error', error, error.stack) 285 | return Promise.reject(error) 286 | } 287 | -------------------------------------------------------------------------------- /src/bin.js: -------------------------------------------------------------------------------- 1 | import toLower from 'ramda/src/toLower' 2 | import Rusha from 'rusha' 3 | import * as CryptoJSlib from '@goodmind/node-cryptojs-aes' 4 | const { CryptoJS } = CryptoJSlib 5 | import { inflate } from 'pako/lib/inflate' 6 | 7 | import random from './service/secure-random' 8 | 9 | 10 | import { eGCD_, greater, divide_, str2bigInt, equalsInt, 11 | isZero, bigInt2str, copy_, copyInt_, rightShift_, 12 | leftShift_, sub_, add_, powMod, bpe, one } from './vendor/leemon' 13 | 14 | 15 | const rushaInstance = new Rusha(1024 * 1024) 16 | 17 | 18 | 19 | export function stringToChars(str: string) { 20 | const ln = str.length 21 | const result: number[] = Array(ln) 22 | for (let i = 0; i < ln; ++i) 23 | result[i] = str.charCodeAt(i) 24 | return result 25 | } 26 | 27 | export const strDecToHex = str => toLower( 28 | bigInt2str( 29 | str2bigInt(str, 10, 0), 16 30 | )) 31 | 32 | export function bytesToHex(bytes = []) { 33 | const arr = [] 34 | for (let i = 0; i < bytes.length; i++) { 35 | arr.push((bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16)) 36 | } 37 | return arr.join('') 38 | } 39 | 40 | export function bytesFromHex(hexString: string) { 41 | const len = hexString.length 42 | let start = 0 43 | const bytes = [] 44 | 45 | if (hexString.length % 2) { 46 | bytes.push(parseInt(hexString.charAt(0), 16)) 47 | start++ 48 | } 49 | 50 | for (let i = start; i < len; i += 2) { 51 | bytes.push(parseInt(hexString.substr(i, 2), 16)) 52 | } 53 | 54 | return bytes 55 | } 56 | 57 | export function bytesCmp(bytes1, bytes2) { 58 | const len = bytes1.length 59 | if (len !== bytes2.length) { 60 | return false 61 | } 62 | 63 | for (let i = 0; i < len; i++) { 64 | if (bytes1[i] !== bytes2[i]) 65 | return false 66 | } 67 | return true 68 | } 69 | 70 | export function bytesXor(bytes1, bytes2) { 71 | const len = bytes1.length 72 | const bytes = [] 73 | 74 | for (let i = 0; i < len; ++i) { 75 | bytes[i] = bytes1[i] ^ bytes2[i] 76 | } 77 | 78 | return bytes 79 | } 80 | 81 | export function bytesToWords(bytes) { 82 | if (bytes instanceof ArrayBuffer) { 83 | bytes = new Uint8Array(bytes) 84 | } 85 | const len = bytes.length 86 | const words = [] 87 | let i 88 | for (i = 0; i < len; i++) { 89 | words[i >>> 2] |= bytes[i] << 24 - i % 4 * 8 90 | } 91 | 92 | return new CryptoJS.lib.WordArray.init(words, len) 93 | } 94 | 95 | export function bytesFromWords(wordArray) { 96 | const words = wordArray.words 97 | const sigBytes = wordArray.sigBytes 98 | const bytes = [] 99 | 100 | for (let i = 0; i < sigBytes; i++) { 101 | bytes.push(words[i >>> 2] >>> 24 - i % 4 * 8 & 0xff) 102 | } 103 | 104 | return bytes 105 | } 106 | 107 | 108 | export function bytesFromLeemonBigInt(bigInt) { 109 | const str = bigInt2str(bigInt, 16) 110 | return bytesFromHex(str) 111 | } 112 | 113 | export function bytesToArrayBuffer(b) { 114 | return (new Uint8Array(b)).buffer 115 | } 116 | 117 | export function convertToArrayBuffer(bytes) { 118 | // Be careful with converting subarrays!! 119 | if (bytes instanceof ArrayBuffer) { 120 | return bytes 121 | } 122 | if (bytes.buffer !== undefined && 123 | bytes.buffer.byteLength == bytes.length * bytes.BYTES_PER_ELEMENT) { 124 | return bytes.buffer 125 | } 126 | return bytesToArrayBuffer(bytes) 127 | } 128 | 129 | export function convertToUint8Array(bytes) { 130 | if (bytes.buffer !== undefined) 131 | return bytes 132 | return new Uint8Array(bytes) 133 | } 134 | 135 | export function convertToByteArray(bytes) { 136 | if (Array.isArray(bytes)) 137 | return bytes 138 | bytes = convertToUint8Array(bytes) 139 | const newBytes = [] 140 | for (let i = 0, len = bytes.length; i < len; i++) 141 | newBytes.push(bytes[i]) 142 | return newBytes 143 | } 144 | 145 | export function bytesFromArrayBuffer(buffer) { 146 | const byteView = new Uint8Array(buffer) 147 | const bytes = Array.from( byteView ) 148 | return bytes 149 | } 150 | 151 | export function bufferConcat(buffer1, buffer2) { 152 | const l1 = buffer1.byteLength || buffer1.length 153 | const l2 = buffer2.byteLength || buffer2.length 154 | const tmp = new Uint8Array(l1 + l2) 155 | tmp.set( 156 | buffer1 instanceof ArrayBuffer 157 | ? new Uint8Array(buffer1) 158 | : buffer1, 159 | 0) 160 | tmp.set( 161 | buffer2 instanceof ArrayBuffer 162 | ? new Uint8Array(buffer2) 163 | : buffer2, 164 | l1) 165 | 166 | return tmp.buffer 167 | } 168 | 169 | // const dividerBig = bigint(0x100000000) 170 | const dividerLem = str2bigInt('100000000', 16, 4) 171 | 172 | // const printTimers = (timeL, timeB, a, b, n) => setTimeout( 173 | // () => console.log(`Timer L ${timeL} B ${timeB}`, ...a, ...b, n || ''), 174 | // 100) 175 | 176 | export function longToInts(sLong: string) { 177 | const lemNum = str2bigInt(sLong, 10, 6) 178 | const div = new Array(lemNum.length) 179 | const rem = new Array(lemNum.length) 180 | divide_(lemNum, dividerLem, div, rem) 181 | const resL = [ 182 | ~~bigInt2str(div, 10), 183 | ~~bigInt2str(rem, 10) 184 | ] 185 | return resL 186 | } 187 | 188 | export function longToBytes(sLong) { 189 | return bytesFromWords({ words: longToInts(sLong), sigBytes: 8 }).reverse() 190 | } 191 | 192 | export function lshift32(high, low) { 193 | const highNum = str2bigInt(high.toString(), 10, 6) 194 | const nLow = str2bigInt(low.toString(), 10, 6) 195 | leftShift_(highNum, 32) 196 | 197 | add_(highNum, nLow) 198 | const res = bigInt2str(highNum, 10) 199 | return res 200 | } 201 | 202 | export const rshift32 = str => { 203 | const num = str2bigInt(str, 10, 6) 204 | rightShift_(num, 32) 205 | return bigInt2str(num, 10) 206 | } 207 | 208 | export function intToUint(val: string) { 209 | let result = ~~val 210 | if (result < 0) 211 | result = result + 0x100000000 212 | return result 213 | } 214 | 215 | const middle = 0x100000000 / 2 - 1 216 | 217 | export function uintToInt(val: number): number { 218 | if (val > middle) 219 | val = val - 0x100000000 220 | return val 221 | } 222 | 223 | export function sha1HashSync(bytes) { 224 | // console.log(dT(), 'SHA-1 hash start', bytes.byteLength || bytes.length) 225 | const hashBytes = rushaInstance.rawDigest(bytes).buffer 226 | // console.log(dT(), 'SHA-1 hash finish') 227 | 228 | return hashBytes 229 | } 230 | 231 | export function sha1BytesSync(bytes) { 232 | return bytesFromArrayBuffer(sha1HashSync(bytes)) 233 | } 234 | 235 | export function sha256HashSync(bytes) { 236 | // console.log(dT(), 'SHA-2 hash start', bytes.byteLength || bytes.length) 237 | const hashWords = CryptoJS.SHA256(bytesToWords(bytes)) 238 | // console.log(dT(), 'SHA-2 hash finish') 239 | 240 | const hashBytes = bytesFromWords(hashWords) 241 | 242 | return hashBytes 243 | } 244 | 245 | export function rsaEncrypt(publicKey, bytes) { 246 | bytes = addPadding(bytes, 255) 247 | 248 | const N = str2bigInt(publicKey.modulus, 16, 256) 249 | const E = str2bigInt(publicKey.exponent, 16, 256) 250 | const X = str2bigInt(bytesToHex(bytes), 16, 256) 251 | const encryptedBigInt = powMod(X, E, N), 252 | encryptedBytes = bytesFromHex(bigInt2str(encryptedBigInt, 16)) 253 | 254 | return encryptedBytes 255 | } 256 | 257 | export function addPadding(bytes, blockSize, zeroes) { 258 | blockSize = blockSize || 16 259 | const len = bytes.byteLength || bytes.length 260 | const needPadding = blockSize - len % blockSize 261 | if (needPadding > 0 && needPadding < blockSize) { 262 | const padding = new Array(needPadding) 263 | if (zeroes) { 264 | for (let i = 0; i < needPadding; i++) 265 | padding[i] = 0 266 | } else 267 | random(padding) 268 | 269 | bytes = bytes instanceof ArrayBuffer 270 | ? bufferConcat(bytes, padding) 271 | : bytes.concat(padding) 272 | } 273 | 274 | return bytes 275 | } 276 | 277 | export function aesEncryptSync(bytes, keyBytes, ivBytes) { 278 | // console.log(dT(), 'AES encrypt start', len/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/) 279 | bytes = addPadding(bytes) 280 | 281 | const encryptedWords = CryptoJS.AES.encrypt(bytesToWords(bytes), bytesToWords(keyBytes), { 282 | iv : bytesToWords(ivBytes), 283 | padding: CryptoJS.pad.NoPadding, 284 | mode : CryptoJS.mode.IGE 285 | }).ciphertext 286 | 287 | const encryptedBytes = bytesFromWords(encryptedWords) 288 | // console.log(dT(), 'AES encrypt finish') 289 | 290 | return encryptedBytes 291 | } 292 | 293 | export function aesDecryptSync(encryptedBytes, keyBytes, ivBytes) { 294 | 295 | // console.log(dT(), 'AES decrypt start', encryptedBytes.length) 296 | const decryptedWords = CryptoJS.AES.decrypt({ ciphertext: bytesToWords(encryptedBytes) }, bytesToWords(keyBytes), { 297 | iv : bytesToWords(ivBytes), 298 | padding: CryptoJS.pad.NoPadding, 299 | mode : CryptoJS.mode.IGE 300 | }) 301 | 302 | const bytes = bytesFromWords(decryptedWords) 303 | // console.log(dT(), 'AES decrypt finish') 304 | 305 | return bytes 306 | } 307 | 308 | export function gzipUncompress(bytes) { 309 | // console.log('Gzip uncompress start') 310 | const result = inflate(bytes) 311 | // console.log('Gzip uncompress finish') 312 | return result 313 | } 314 | 315 | export function nextRandomInt(maxValue) { 316 | return Math.floor(Math.random() * maxValue) 317 | } 318 | 319 | 320 | export function pqPrimeFactorization(pqBytes) { 321 | const minSize = Math.ceil(64 / bpe) + 1 322 | 323 | // const what = new BigInteger(pqBytes) 324 | const hex = bytesToHex(pqBytes) 325 | const lWhat = str2bigInt(hex, 16, minSize) 326 | const result = pqPrimeLeemon(lWhat) 327 | return result 328 | } 329 | 330 | 331 | export function pqPrimeLeemon(what) { 332 | const minBits = 64 333 | const minLen = Math.ceil(minBits / bpe) + 1 334 | let it = 0 335 | let q, lim 336 | const a = new Array(minLen) 337 | const b = new Array(minLen) 338 | const c = new Array(minLen) 339 | const g = new Array(minLen) 340 | const z = new Array(minLen) 341 | const x = new Array(minLen) 342 | const y = new Array(minLen) 343 | 344 | for (let i = 0; i < 3; i++) { 345 | q = (nextRandomInt(128) & 15) + 17 346 | copyInt_(x, nextRandomInt(1000000000) + 1) 347 | copy_(y, x) 348 | lim = 1 << i + 18 349 | 350 | for (let j = 1; j < lim; j++) { 351 | ++it 352 | copy_(a, x) 353 | copy_(b, x) 354 | copyInt_(c, q) 355 | 356 | while (!isZero(b)) { 357 | if (b[0] & 1) { 358 | add_(c, a) 359 | if (greater(c, what)) { 360 | sub_(c, what) 361 | } 362 | } 363 | add_(a, a) 364 | if (greater(a, what)) { 365 | sub_(a, what) 366 | } 367 | rightShift_(b, 1) 368 | } 369 | 370 | copy_(x, c) 371 | if (greater(x, y)) { 372 | copy_(z, x) 373 | sub_(z, y) 374 | } else { 375 | copy_(z, y) 376 | sub_(z, x) 377 | } 378 | eGCD_(z, what, g, a, b) 379 | if (!equalsInt(g, 1)) { 380 | break 381 | } 382 | if ((j & j - 1) === 0) { 383 | copy_(y, x) 384 | } 385 | } 386 | if (greater(g, one)) { 387 | break 388 | } 389 | } 390 | 391 | divide_(what, g, x, y) 392 | 393 | const [P, Q] = 394 | greater(g, x) 395 | ? [x, g] 396 | : [g, x] 397 | 398 | // console.log(dT(), 'done', bigInt2str(what, 10), bigInt2str(P, 10), bigInt2str(Q, 10)) 399 | 400 | return [bytesFromLeemonBigInt(P), bytesFromLeemonBigInt(Q), it] 401 | } 402 | 403 | export function bytesModPow(x, y, m) { 404 | const xBigInt = str2bigInt(bytesToHex(x), 16) 405 | const yBigInt = str2bigInt(bytesToHex(y), 16) 406 | const mBigInt = str2bigInt(bytesToHex(m), 16) 407 | const resBigInt = powMod(xBigInt, yBigInt, mBigInt) 408 | 409 | return bytesFromHex(bigInt2str(resBigInt, 16)) 410 | } 411 | 412 | -------------------------------------------------------------------------------- /schema/mtproto-57.json: -------------------------------------------------------------------------------- 1 | { 2 | "constructors": [{ 3 | "id": "481674261", 4 | "predicate": "vector", 5 | "params": [], 6 | "type": "Vector t" 7 | }, { 8 | "id": "85337187", 9 | "predicate": "resPQ", 10 | "params": [{ 11 | "name": "nonce", 12 | "type": "int128" 13 | }, { 14 | "name": "server_nonce", 15 | "type": "int128" 16 | }, { 17 | "name": "pq", 18 | "type": "bytes" 19 | }, { 20 | "name": "server_public_key_fingerprints", 21 | "type": "Vector" 22 | }], 23 | "type": "ResPQ" 24 | }, { 25 | "id": "-2083955988", 26 | "predicate": "p_q_inner_data", 27 | "params": [{ 28 | "name": "pq", 29 | "type": "bytes" 30 | }, { 31 | "name": "p", 32 | "type": "bytes" 33 | }, { 34 | "name": "q", 35 | "type": "bytes" 36 | }, { 37 | "name": "nonce", 38 | "type": "int128" 39 | }, { 40 | "name": "server_nonce", 41 | "type": "int128" 42 | }, { 43 | "name": "new_nonce", 44 | "type": "int256" 45 | }], 46 | "type": "P_Q_inner_data" 47 | }, { 48 | "id": "2043348061", 49 | "predicate": "server_DH_params_fail", 50 | "params": [{ 51 | "name": "nonce", 52 | "type": "int128" 53 | }, { 54 | "name": "server_nonce", 55 | "type": "int128" 56 | }, { 57 | "name": "new_nonce_hash", 58 | "type": "int128" 59 | }], 60 | "type": "Server_DH_Params" 61 | }, { 62 | "id": "-790100132", 63 | "predicate": "server_DH_params_ok", 64 | "params": [{ 65 | "name": "nonce", 66 | "type": "int128" 67 | }, { 68 | "name": "server_nonce", 69 | "type": "int128" 70 | }, { 71 | "name": "encrypted_answer", 72 | "type": "bytes" 73 | }], 74 | "type": "Server_DH_Params" 75 | }, { 76 | "id": "-1249309254", 77 | "predicate": "server_DH_inner_data", 78 | "params": [{ 79 | "name": "nonce", 80 | "type": "int128" 81 | }, { 82 | "name": "server_nonce", 83 | "type": "int128" 84 | }, { 85 | "name": "g", 86 | "type": "int" 87 | }, { 88 | "name": "dh_prime", 89 | "type": "bytes" 90 | }, { 91 | "name": "g_a", 92 | "type": "bytes" 93 | }, { 94 | "name": "server_time", 95 | "type": "int" 96 | }], 97 | "type": "Server_DH_inner_data" 98 | }, { 99 | "id": "1715713620", 100 | "predicate": "client_DH_inner_data", 101 | "params": [{ 102 | "name": "nonce", 103 | "type": "int128" 104 | }, { 105 | "name": "server_nonce", 106 | "type": "int128" 107 | }, { 108 | "name": "retry_id", 109 | "type": "long" 110 | }, { 111 | "name": "g_b", 112 | "type": "bytes" 113 | }], 114 | "type": "Client_DH_Inner_Data" 115 | }, { 116 | "id": "1003222836", 117 | "predicate": "dh_gen_ok", 118 | "params": [{ 119 | "name": "nonce", 120 | "type": "int128" 121 | }, { 122 | "name": "server_nonce", 123 | "type": "int128" 124 | }, { 125 | "name": "new_nonce_hash1", 126 | "type": "int128" 127 | }], 128 | "type": "Set_client_DH_params_answer" 129 | }, { 130 | "id": "1188831161", 131 | "predicate": "dh_gen_retry", 132 | "params": [{ 133 | "name": "nonce", 134 | "type": "int128" 135 | }, { 136 | "name": "server_nonce", 137 | "type": "int128" 138 | }, { 139 | "name": "new_nonce_hash2", 140 | "type": "int128" 141 | }], 142 | "type": "Set_client_DH_params_answer" 143 | }, { 144 | "id": "-1499615742", 145 | "predicate": "dh_gen_fail", 146 | "params": [{ 147 | "name": "nonce", 148 | "type": "int128" 149 | }, { 150 | "name": "server_nonce", 151 | "type": "int128" 152 | }, { 153 | "name": "new_nonce_hash3", 154 | "type": "int128" 155 | }], 156 | "type": "Set_client_DH_params_answer" 157 | }, { 158 | "id": "-212046591", 159 | "predicate": "rpc_result", 160 | "params": [{ 161 | "name": "req_msg_id", 162 | "type": "long" 163 | }, { 164 | "name": "result", 165 | "type": "Object" 166 | }], 167 | "type": "RpcResult" 168 | }, { 169 | "id": "558156313", 170 | "predicate": "rpc_error", 171 | "params": [{ 172 | "name": "error_code", 173 | "type": "int" 174 | }, { 175 | "name": "error_message", 176 | "type": "string" 177 | }], 178 | "type": "RpcError" 179 | }, { 180 | "id": "1579864942", 181 | "predicate": "rpc_answer_unknown", 182 | "params": [], 183 | "type": "RpcDropAnswer" 184 | }, { 185 | "id": "-847714938", 186 | "predicate": "rpc_answer_dropped_running", 187 | "params": [], 188 | "type": "RpcDropAnswer" 189 | }, { 190 | "id": "-1539647305", 191 | "predicate": "rpc_answer_dropped", 192 | "params": [{ 193 | "name": "msg_id", 194 | "type": "long" 195 | }, { 196 | "name": "seq_no", 197 | "type": "int" 198 | }, { 199 | "name": "bytes", 200 | "type": "int" 201 | }], 202 | "type": "RpcDropAnswer" 203 | }, { 204 | "id": "155834844", 205 | "predicate": "future_salt", 206 | "params": [{ 207 | "name": "valid_since", 208 | "type": "int" 209 | }, { 210 | "name": "valid_until", 211 | "type": "int" 212 | }, { 213 | "name": "salt", 214 | "type": "long" 215 | }], 216 | "type": "FutureSalt" 217 | }, { 218 | "id": "-1370486635", 219 | "predicate": "future_salts", 220 | "params": [{ 221 | "name": "req_msg_id", 222 | "type": "long" 223 | }, { 224 | "name": "now", 225 | "type": "int" 226 | }, { 227 | "name": "salts", 228 | "type": "vector" 229 | }], 230 | "type": "FutureSalts" 231 | }, { 232 | "id": "880243653", 233 | "predicate": "pong", 234 | "params": [{ 235 | "name": "msg_id", 236 | "type": "long" 237 | }, { 238 | "name": "ping_id", 239 | "type": "long" 240 | }], 241 | "type": "Pong" 242 | }, { 243 | "id": "-501201412", 244 | "predicate": "destroy_session_ok", 245 | "params": [{ 246 | "name": "session_id", 247 | "type": "long" 248 | }], 249 | "type": "DestroySessionRes" 250 | }, { 251 | "id": "1658015945", 252 | "predicate": "destroy_session_none", 253 | "params": [{ 254 | "name": "session_id", 255 | "type": "long" 256 | }], 257 | "type": "DestroySessionRes" 258 | }, { 259 | "id": "-1631450872", 260 | "predicate": "new_session_created", 261 | "params": [{ 262 | "name": "first_msg_id", 263 | "type": "long" 264 | }, { 265 | "name": "unique_id", 266 | "type": "long" 267 | }, { 268 | "name": "server_salt", 269 | "type": "long" 270 | }], 271 | "type": "NewSession" 272 | }, { 273 | "id": "1945237724", 274 | "predicate": "msg_container", 275 | "params": [{ 276 | "name": "messages", 277 | "type": "vector<%Message>" 278 | }], 279 | "type": "MessageContainer" 280 | }, { 281 | "id": "1538843921", 282 | "predicate": "message", 283 | "params": [{ 284 | "name": "msg_id", 285 | "type": "long" 286 | }, { 287 | "name": "seqno", 288 | "type": "int" 289 | }, { 290 | "name": "bytes", 291 | "type": "int" 292 | }, { 293 | "name": "body", 294 | "type": "Object" 295 | }], 296 | "type": "Message" 297 | }, { 298 | "id": "-530561358", 299 | "predicate": "msg_copy", 300 | "params": [{ 301 | "name": "orig_message", 302 | "type": "Message" 303 | }], 304 | "type": "MessageCopy" 305 | }, { 306 | "id": "812830625", 307 | "predicate": "gzip_packed", 308 | "params": [{ 309 | "name": "packed_data", 310 | "type": "bytes" 311 | }], 312 | "type": "Object" 313 | }, { 314 | "id": "1658238041", 315 | "predicate": "msgs_ack", 316 | "params": [{ 317 | "name": "msg_ids", 318 | "type": "Vector" 319 | }], 320 | "type": "MsgsAck" 321 | }, { 322 | "id": "-1477445615", 323 | "predicate": "bad_msg_notification", 324 | "params": [{ 325 | "name": "bad_msg_id", 326 | "type": "long" 327 | }, { 328 | "name": "bad_msg_seqno", 329 | "type": "int" 330 | }, { 331 | "name": "error_code", 332 | "type": "int" 333 | }], 334 | "type": "BadMsgNotification" 335 | }, { 336 | "id": "-307542917", 337 | "predicate": "bad_server_salt", 338 | "params": [{ 339 | "name": "bad_msg_id", 340 | "type": "long" 341 | }, { 342 | "name": "bad_msg_seqno", 343 | "type": "int" 344 | }, { 345 | "name": "error_code", 346 | "type": "int" 347 | }, { 348 | "name": "new_server_salt", 349 | "type": "long" 350 | }], 351 | "type": "BadMsgNotification" 352 | }, { 353 | "id": "2105940488", 354 | "predicate": "msg_resend_req", 355 | "params": [{ 356 | "name": "msg_ids", 357 | "type": "Vector" 358 | }], 359 | "type": "MsgResendReq" 360 | }, { 361 | "id": "-630588590", 362 | "predicate": "msgs_state_req", 363 | "params": [{ 364 | "name": "msg_ids", 365 | "type": "Vector" 366 | }], 367 | "type": "MsgsStateReq" 368 | }, { 369 | "id": "81704317", 370 | "predicate": "msgs_state_info", 371 | "params": [{ 372 | "name": "req_msg_id", 373 | "type": "long" 374 | }, { 375 | "name": "info", 376 | "type": "bytes" 377 | }], 378 | "type": "MsgsStateInfo" 379 | }, { 380 | "id": "-1933520591", 381 | "predicate": "msgs_all_info", 382 | "params": [{ 383 | "name": "msg_ids", 384 | "type": "Vector" 385 | }, { 386 | "name": "info", 387 | "type": "bytes" 388 | }], 389 | "type": "MsgsAllInfo" 390 | }, { 391 | "id": "661470918", 392 | "predicate": "msg_detailed_info", 393 | "params": [{ 394 | "name": "msg_id", 395 | "type": "long" 396 | }, { 397 | "name": "answer_msg_id", 398 | "type": "long" 399 | }, { 400 | "name": "bytes", 401 | "type": "int" 402 | }, { 403 | "name": "status", 404 | "type": "int" 405 | }], 406 | "type": "MsgDetailedInfo" 407 | }, { 408 | "id": "-2137147681", 409 | "predicate": "msg_new_detailed_info", 410 | "params": [{ 411 | "name": "answer_msg_id", 412 | "type": "long" 413 | }, { 414 | "name": "bytes", 415 | "type": "int" 416 | }, { 417 | "name": "status", 418 | "type": "int" 419 | }], 420 | "type": "MsgDetailedInfo" 421 | }], 422 | "methods": [{ 423 | "id": "1615239032", 424 | "method": "req_pq", 425 | "params": [{ 426 | "name": "nonce", 427 | "type": "int128" 428 | }], 429 | "type": "ResPQ" 430 | }, { 431 | "id": "-686627650", 432 | "method": "req_DH_params", 433 | "params": [{ 434 | "name": "nonce", 435 | "type": "int128" 436 | }, { 437 | "name": "server_nonce", 438 | "type": "int128" 439 | }, { 440 | "name": "p", 441 | "type": "bytes" 442 | }, { 443 | "name": "q", 444 | "type": "bytes" 445 | }, { 446 | "name": "public_key_fingerprint", 447 | "type": "long" 448 | }, { 449 | "name": "encrypted_data", 450 | "type": "bytes" 451 | }], 452 | "type": "Server_DH_Params" 453 | }, { 454 | "id": "-184262881", 455 | "method": "set_client_DH_params", 456 | "params": [{ 457 | "name": "nonce", 458 | "type": "int128" 459 | }, { 460 | "name": "server_nonce", 461 | "type": "int128" 462 | }, { 463 | "name": "encrypted_data", 464 | "type": "bytes" 465 | }], 466 | "type": "Set_client_DH_params_answer" 467 | }, { 468 | "id": "1491380032", 469 | "method": "rpc_drop_answer", 470 | "params": [{ 471 | "name": "req_msg_id", 472 | "type": "long" 473 | }], 474 | "type": "RpcDropAnswer" 475 | }, { 476 | "id": "-1188971260", 477 | "method": "get_future_salts", 478 | "params": [{ 479 | "name": "num", 480 | "type": "int" 481 | }], 482 | "type": "FutureSalts" 483 | }, { 484 | "id": "2059302892", 485 | "method": "ping", 486 | "params": [{ 487 | "name": "ping_id", 488 | "type": "long" 489 | }], 490 | "type": "Pong" 491 | }, { 492 | "id": "-213746804", 493 | "method": "ping_delay_disconnect", 494 | "params": [{ 495 | "name": "ping_id", 496 | "type": "long" 497 | }, { 498 | "name": "disconnect_delay", 499 | "type": "int" 500 | }], 501 | "type": "Pong" 502 | }, { 503 | "id": "-414113498", 504 | "method": "destroy_session", 505 | "params": [{ 506 | "name": "session_id", 507 | "type": "long" 508 | }], 509 | "type": "DestroySessionRes" 510 | }, { 511 | "id": "-1835453025", 512 | "method": "http_wait", 513 | "params": [{ 514 | "name": "max_delay", 515 | "type": "int" 516 | }, { 517 | "name": "wait_after", 518 | "type": "int" 519 | }, { 520 | "name": "max_wait", 521 | "type": "int" 522 | }], 523 | "type": "HttpWait" 524 | }] 525 | } -------------------------------------------------------------------------------- /logs/flood.log: -------------------------------------------------------------------------------- 1 | [42.374] Api call messages.getHistory 2 | [42.730] Rpc response messages.channelMessages 3 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 4 | ttp://goo.gl/rRqMUw 5 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 6 | [42.738] Api call messages.getHistory 7 | [43.096] Rpc response messages.channelMessages 8 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 9 | ttp://goo.gl/rRqMUw 10 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 11 | [43.104] Api call messages.getHistory 12 | [43.460] Rpc response messages.channelMessages 13 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 14 | ttp://goo.gl/rRqMUw 15 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 16 | [43.468] Api call messages.getHistory 17 | [43.848] Rpc response messages.channelMessages 18 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 19 | ttp://goo.gl/rRqMUw 20 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 21 | [43.882] Api call messages.getHistory 22 | [44.232] Rpc response messages.channelMessages 23 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 24 | ttp://goo.gl/rRqMUw 25 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 26 | [44.241] Api call messages.getHistory 27 | [44.609] Rpc response messages.channelMessages 28 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 29 | ttp://goo.gl/rRqMUw 30 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 31 | [44.615] Api call messages.getHistory 32 | [44.964] Rpc response messages.channelMessages 33 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 34 | ttp://goo.gl/rRqMUw 35 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 36 | [44.974] Api call messages.getHistory 37 | [45.338] Rpc response messages.channelMessages 38 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 39 | ttp://goo.gl/rRqMUw 40 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 41 | [45.346] Api call messages.getHistory 42 | [45.697] Rpc response messages.channelMessages 43 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 44 | ttp://goo.gl/rRqMUw 45 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 46 | [45.704] Api call messages.getHistory 47 | [46.055] Rpc response messages.channelMessages 48 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 49 | ttp://goo.gl/rRqMUw 50 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 51 | [46.064] Api call messages.getHistory 52 | [46.441] Rpc response messages.channelMessages 53 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 54 | ttp://goo.gl/rRqMUw 55 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 56 | [46.452] Api call messages.getHistory 57 | [46.792] Rpc response messages.channelMessages 58 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 59 | ttp://goo.gl/rRqMUw 60 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 61 | [46.800] Api call messages.getHistory 62 | [47.143] Rpc response messages.channelMessages 63 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 64 | ttp://goo.gl/rRqMUw 65 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 66 | [47.152] Api call messages.getHistory 67 | [47.510] Rpc response messages.channelMessages 68 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 69 | ttp://goo.gl/rRqMUw 70 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 71 | [47.516] Api call messages.getHistory 72 | [47.861] Rpc response messages.channelMessages 73 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 74 | ttp://goo.gl/rRqMUw 75 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 76 | [47.867] Api call messages.getHistory 77 | [48.206] Rpc response messages.channelMessages 78 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 79 | ttp://goo.gl/rRqMUw 80 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 81 | [48.213] Api call messages.getHistory 82 | [48.553] Rpc response messages.channelMessages 83 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 84 | ttp://goo.gl/rRqMUw 85 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 86 | [48.565] Api call messages.getHistory 87 | [48.921] Rpc response messages.channelMessages 88 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 89 | ttp://goo.gl/rRqMUw 90 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 91 | [48.927] Api call messages.getHistory 92 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 93 | ttp://goo.gl/rRqMUw 94 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 95 | [49.268] Rpc response messages.channelMessages 96 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 97 | ttp://goo.gl/rRqMUw 98 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 99 | [49.275] Api call messages.getHistory 100 | [49.631] Rpc response messages.channelMessages 101 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 102 | ttp://goo.gl/rRqMUw 103 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 104 | [49.636] Api call messages.getHistory 105 | [49.995] Rpc response messages.channelMessages 106 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 107 | ttp://goo.gl/rRqMUw 108 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 109 | [50.000] Api call messages.getHistory 110 | [50.372] Rpc response messages.channelMessages 111 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 112 | ttp://goo.gl/rRqMUw 113 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 114 | [50.383] Api call messages.getHistory 115 | [50.744] Rpc response messages.channelMessages 116 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 117 | ttp://goo.gl/rRqMUw 118 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 119 | [50.750] Api call messages.getHistory 120 | [51.085] Rpc response messages.channelMessages 121 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 122 | ttp://goo.gl/rRqMUw 123 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 124 | [51.091] Api call messages.getHistory 125 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 126 | ttp://goo.gl/rRqMUw 127 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 128 | [51.442] Rpc response messages.channelMessages 129 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 130 | ttp://goo.gl/rRqMUw 131 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 132 | [51.446] Api call messages.getHistory 133 | [51.811] Rpc response messages.channelMessages 134 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 135 | ttp://goo.gl/rRqMUw 136 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 137 | [51.819] Api call messages.getHistory 138 | [52.152] Rpc response messages.channelMessages 139 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 140 | ttp://goo.gl/rRqMUw 141 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 142 | [52.163] Api call messages.getHistory 143 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 144 | ttp://goo.gl/rRqMUw 145 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 146 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 147 | ttp://goo.gl/rRqMUw 148 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 149 | [52.561] Rpc response messages.channelMessages 150 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 151 | ttp://goo.gl/rRqMUw 152 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 153 | [52.567] Api call messages.getHistory 154 | [52.937] Rpc response messages.channelMessages 155 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 156 | ttp://goo.gl/rRqMUw 157 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 158 | [52.942] Api call messages.getHistory 159 | [53.303] Rpc response messages.channelMessages 160 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 161 | ttp://goo.gl/rRqMUw 162 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 163 | [53.311] Api call messages.getHistory 164 | [53.631] Rpc error { code: 420, 165 | type: 'FLOOD_WAIT_19', 166 | description: 'CODE#420 FLOOD_WAIT_19', 167 | originalError: 168 | { _: 'rpc_error', 169 | error_code: 420, 170 | error_message: 'FLOOD_WAIT_19' } } 171 | (node:18200) Warning: a promise was rejected with a non-error: [object Object] 172 | (node:18200) Warning: a promise was created in a handler at C:\_projects\Web\mtproto2\lib\service\networker.js:832:14 but was not returned from it, see h 173 | ttp://goo.gl/rRqMUw 174 | at new Promise (C:\_projects\Web\mtproto2\node_modules\bluebird\js\release\promise.js:77:14) 175 | [53.641] Error 420 FLOOD_WAIT_19 2 2 -------------------------------------------------------------------------------- /src/service/updates.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Promise from 'bluebird' 4 | import Logger from '../util/log' 5 | const debug = Logger`updates` 6 | 7 | import { setUpdatesProcessor } from './networker' 8 | import type { ApiManagerInstance } from './api-manager/index.h' 9 | import type { UpdatesState, CurState } from './updates.h' 10 | 11 | // const AppPeersManager = null 12 | // const AppUsersManager = null 13 | const AppChatsManager = null 14 | 15 | const UpdatesManager = (api: ApiManagerInstance) => { 16 | const updatesState: any = { 17 | pendingPtsUpdates: [], 18 | pendingSeqUpdates: {}, 19 | syncPending : false, 20 | syncLoading : true 21 | } 22 | const channelStates = {} 23 | 24 | let myID = 0 25 | getUserID().then(id => myID = id) 26 | 27 | async function getUserID() { 28 | const auth = await api.storage.get('user_auth') 29 | return auth.id || 0 30 | } 31 | 32 | function popPendingSeqUpdate() { 33 | const nextSeq = updatesState.seq + 1 34 | const pendingUpdatesData = updatesState.pendingSeqUpdates[nextSeq] 35 | if (!pendingUpdatesData) { 36 | return false 37 | } 38 | const updates = pendingUpdatesData.updates 39 | updates.forEach(saveUpdate) 40 | updatesState.seq = pendingUpdatesData.seq 41 | if (pendingUpdatesData.date && updatesState.date < pendingUpdatesData.date) { 42 | updatesState.date = pendingUpdatesData.date 43 | } 44 | delete updatesState.pendingSeqUpdates[nextSeq] 45 | 46 | if (!popPendingSeqUpdate() && 47 | updatesState.syncPending && 48 | updatesState.syncPending.seqAwaiting && 49 | updatesState.seq >= updatesState.syncPending.seqAwaiting) { 50 | if (!updatesState.syncPending.ptsAwaiting) { 51 | clearTimeout(updatesState.syncPending.timeout) 52 | updatesState.syncPending = false 53 | } else { 54 | delete updatesState.syncPending.seqAwaiting 55 | } 56 | } 57 | 58 | return true 59 | } 60 | 61 | function popPendingPtsUpdate(channelID) { 62 | const curState = channelID ? getChannelState(channelID) : updatesState 63 | if (!curState.pendingPtsUpdates.length) { 64 | return false 65 | } 66 | curState.pendingPtsUpdates.sort((a, b) => a.pts - b.pts) 67 | 68 | let curPts = curState.pts 69 | let goodPts = false 70 | let goodIndex = 0 71 | let update 72 | let i = 0 73 | for (const update of curState.pendingPtsUpdates) { 74 | curPts += update.pts_count 75 | if (curPts >= update.pts) { 76 | goodPts = update.pts 77 | goodIndex = i 78 | } 79 | i++ 80 | } 81 | 82 | if (!goodPts) { 83 | return false 84 | } 85 | 86 | debug('pop pending pts updates')(goodPts, curState.pendingPtsUpdates.slice(0, goodIndex + 1)) 87 | 88 | curState.pts = goodPts 89 | for (let i = 0; i <= goodIndex; i++) { 90 | update = curState.pendingPtsUpdates[i] 91 | saveUpdate(update) 92 | } 93 | curState.pendingPtsUpdates.splice(0, goodIndex + 1) 94 | 95 | if (!curState.pendingPtsUpdates.length && curState.syncPending) { 96 | if (!curState.syncPending.seqAwaiting) { 97 | clearTimeout(curState.syncPending.timeout) 98 | curState.syncPending = false 99 | } else { 100 | delete curState.syncPending.ptsAwaiting 101 | } 102 | } 103 | 104 | return true 105 | } 106 | 107 | function forceGetDifference() { 108 | if (!updatesState.syncLoading) { 109 | getDifference() 110 | } 111 | } 112 | 113 | function processUpdateMessage(updateMessage: any) { 114 | // return forceGetDifference() 115 | const processOpts = { 116 | date : updateMessage.date, 117 | seq : updateMessage.seq, 118 | seqStart: updateMessage.seq_start 119 | } 120 | 121 | switch (updateMessage._) { 122 | case 'updatesTooLong': 123 | case 'new_session_created': 124 | forceGetDifference() 125 | break 126 | 127 | case 'updateShort': 128 | processUpdate(updateMessage.update, processOpts) 129 | break 130 | 131 | case 'updateShortMessage': 132 | case 'updateShortChatMessage': { 133 | const isOut = updateMessage.flags & 2 134 | const fromID = updateMessage.from_id || (isOut ? myID : updateMessage.user_id) 135 | const toID = updateMessage.chat_id 136 | ? -updateMessage.chat_id 137 | : isOut ? updateMessage.user_id : myID 138 | 139 | api.emit('updateShortMessage', { 140 | processUpdate, 141 | processOpts, 142 | updateMessage, 143 | fromID, 144 | toID 145 | }) 146 | } 147 | break 148 | 149 | case 'updatesCombined': 150 | case 'updates': 151 | api.emit('apiUpdate', updateMessage) 152 | 153 | updateMessage.updates.forEach(update => { 154 | processUpdate(update, processOpts) 155 | }) 156 | break 157 | 158 | default: 159 | debug('Unknown update message')(updateMessage) 160 | } 161 | } 162 | 163 | async function getDifference() { 164 | if (!updatesState.syncLoading) { 165 | updatesState.syncLoading = true 166 | updatesState.pendingSeqUpdates = {} 167 | updatesState.pendingPtsUpdates = [] 168 | } 169 | 170 | if (updatesState.syncPending) { 171 | clearTimeout(updatesState.syncPending.timeout) 172 | updatesState.syncPending = false 173 | } 174 | 175 | const differenceResult = await api('updates.getDifference', { 176 | pts : updatesState.pts, 177 | date: updatesState.date, 178 | qts : -1 179 | }) 180 | if (differenceResult._ === 'updates.differenceEmpty') { 181 | debug('apply empty diff')(differenceResult.seq) 182 | updatesState.date = differenceResult.date 183 | updatesState.seq = differenceResult.seq 184 | updatesState.syncLoading = false 185 | api.emit('stateSynchronized') 186 | return false 187 | } 188 | 189 | api.emit('difference', differenceResult) 190 | 191 | // Should be first because of updateMessageID 192 | // console.log(dT(), 'applying', differenceResult.other_updates.length, 'other updates') 193 | 194 | const channelsUpdates = [] 195 | differenceResult.other_updates.forEach(update => { 196 | switch (update._) { 197 | case 'updateChannelTooLong': 198 | case 'updateNewChannelMessage': 199 | case 'updateEditChannelMessage': 200 | processUpdate(update) 201 | return 202 | } 203 | saveUpdate(update) 204 | }) 205 | 206 | // console.log(dT(), 'applying', differenceResult.new_messages.length, 'new messages') 207 | differenceResult.new_messages.forEach(apiMessage => { 208 | saveUpdate({ 209 | _ : 'updateNewMessage', 210 | message : apiMessage, 211 | pts : updatesState.pts, 212 | pts_count: 0 213 | }) 214 | }) 215 | 216 | const nextState = differenceResult.intermediate_state || differenceResult.state 217 | updatesState.seq = nextState.seq 218 | updatesState.pts = nextState.pts 219 | updatesState.date = nextState.date 220 | 221 | // console.log(dT(), 'apply diff', updatesState.seq, updatesState.pts) 222 | 223 | if (differenceResult._ == 'updates.differenceSlice') { 224 | getDifference() 225 | } else { 226 | // console.log(dT(), 'finished get diff') 227 | api.emit('stateSynchronized') 228 | updatesState.syncLoading = false 229 | } 230 | } 231 | 232 | async function getChannelDifference(channelID: number) { 233 | const channelState = getChannelState(channelID) 234 | if (!channelState.syncLoading) { 235 | channelState.syncLoading = true 236 | channelState.pendingPtsUpdates = [] 237 | } 238 | if (channelState.syncPending) { 239 | clearTimeout(channelState.syncPending.timeout) 240 | channelState.syncPending = false 241 | } 242 | // console.log(dT(), 'Get channel diff', AppChatsManager.getChat(channelID), channelState.pts) 243 | const differenceResult = await api('updates.getChannelDifference', { 244 | channel: AppChatsManager.getChannelInput(channelID), 245 | filter : { _: 'channelMessagesFilterEmpty' }, 246 | pts : channelState.pts, 247 | limit : 30 248 | }) 249 | // console.log(dT(), 'channel diff result', differenceResult) 250 | channelState.pts = differenceResult.pts 251 | 252 | if (differenceResult._ == 'updates.channelDifferenceEmpty') { 253 | debug('apply channel empty diff')(differenceResult) 254 | channelState.syncLoading = false 255 | api.emit('stateSynchronized') 256 | return false 257 | } 258 | 259 | if (differenceResult._ == 'updates.channelDifferenceTooLong') { 260 | debug('channel diff too long')(differenceResult) 261 | channelState.syncLoading = false 262 | delete channelStates[channelID] 263 | saveUpdate({ _: 'updateChannelReload', channel_id: channelID }) 264 | return false 265 | } 266 | 267 | api.emit('difference', differenceResult) 268 | 269 | // Should be first because of updateMessageID 270 | debug('applying')(differenceResult.other_updates.length, 'channel other updates') 271 | differenceResult.other_updates.map(saveUpdate) 272 | 273 | debug('applying')(differenceResult.new_messages.length, 'channel new messages') 274 | differenceResult.new_messages.forEach(apiMessage => { 275 | saveUpdate({ 276 | _ : 'updateNewChannelMessage', 277 | message : apiMessage, 278 | pts : channelState.pts, 279 | pts_count: 0 280 | }) 281 | }) 282 | 283 | debug('apply channel diff')(channelState.pts) 284 | 285 | if (differenceResult._ == 'updates.channelDifference' && 286 | !differenceResult.pFlags['final']) { 287 | getChannelDifference(channelID) 288 | } else { 289 | debug('finished channel get diff')() 290 | api.emit('stateSynchronized') 291 | channelState.syncLoading = false 292 | } 293 | } 294 | 295 | function addChannelState(channelID: number, pts: ?number) { 296 | if (!pts) { 297 | throw new Error(`Add channel state without pts ${channelID}`) 298 | } 299 | if (channelStates[channelID] === undefined) { 300 | channelStates[channelID] = { 301 | pts, 302 | pendingPtsUpdates: [], 303 | syncPending : false, 304 | syncLoading : false 305 | } 306 | return true 307 | } 308 | return false 309 | } 310 | 311 | function getChannelState(channelID: number, pts?: ?number) { 312 | if (channelStates[channelID] === undefined) { 313 | addChannelState(channelID, pts) 314 | } 315 | return channelStates[channelID] 316 | } 317 | 318 | function processUpdate(update, options = {}) { 319 | let channelID 320 | switch (update._) { 321 | case 'updateNewChannelMessage': 322 | case 'updateEditChannelMessage': 323 | channelID = update.message.to_id.channel_id || update.message.to_id.chat_id 324 | break 325 | case 'updateDeleteChannelMessages': 326 | channelID = update.channel_id 327 | break 328 | case 'updateChannelTooLong': 329 | channelID = update.channel_id 330 | if (channelStates[channelID] === undefined) { 331 | return false 332 | } 333 | break 334 | } 335 | 336 | const curState: CurState = channelID ? getChannelState(channelID, update.pts) : updatesState 337 | 338 | // console.log(dT(), 'process', channelID, curState.pts, update) 339 | 340 | if (curState.syncLoading) { 341 | return false 342 | } 343 | 344 | if (update._ == 'updateChannelTooLong') { 345 | getChannelDifference(channelID || 0) 346 | return false 347 | } 348 | 349 | let popPts 350 | let popSeq 351 | 352 | if (update.pts) { 353 | const newPts = curState.pts + (update.pts_count || 0) 354 | if (newPts < update.pts) { 355 | // debug('Pts hole')(curState, update, channelID && AppChatsManager.getChat(channelID)) 356 | curState.pendingPtsUpdates.push(update) 357 | if (!curState.syncPending) { 358 | curState.syncPending = { 359 | timeout: setTimeout(() => { 360 | if (channelID) { 361 | getChannelDifference(channelID) 362 | } else { 363 | getDifference() 364 | } 365 | }, 5000), 366 | } 367 | } 368 | curState.syncPending.ptsAwaiting = true 369 | return false 370 | } 371 | if (update.pts > curState.pts) { 372 | curState.pts = update.pts 373 | popPts = true 374 | } 375 | else if (update.pts_count) { 376 | // console.warn(dT(), 'Duplicate update', update) 377 | return false 378 | } 379 | if (channelID && options.date && updatesState.date < options.date) { 380 | updatesState.date = options.date 381 | } 382 | } 383 | else if (!channelID && options.seq > 0) { 384 | const seq = options.seq 385 | const seqStart = options.seqStart || seq 386 | 387 | if (seqStart != curState.seq + 1) { 388 | if (seqStart > curState.seq) { 389 | debug('Seq hole')(curState, curState.syncPending && curState.syncPending.seqAwaiting) 390 | 391 | if (curState.pendingSeqUpdates[seqStart] === undefined) { 392 | curState.pendingSeqUpdates[seqStart] = { seq, date: options.date, updates: [] } 393 | } 394 | curState.pendingSeqUpdates[seqStart].updates.push(update) 395 | 396 | if (!curState.syncPending) { 397 | curState.syncPending = { 398 | timeout: setTimeout(() => { 399 | getDifference() 400 | }, 5000) 401 | } 402 | } 403 | if (!curState.syncPending.seqAwaiting || 404 | curState.syncPending.seqAwaiting < seqStart) { 405 | curState.syncPending.seqAwaiting = seqStart 406 | } 407 | return false 408 | } 409 | } 410 | 411 | if (curState.seq != seq) { 412 | curState.seq = seq 413 | if (options.date && curState.date < options.date) { 414 | curState.date = options.date 415 | } 416 | popSeq = true 417 | } 418 | } 419 | 420 | saveUpdate(update) 421 | 422 | if (popPts) { 423 | popPendingPtsUpdate(channelID) 424 | } 425 | else if (popSeq) { 426 | popPendingSeqUpdate() 427 | } 428 | } 429 | 430 | function saveUpdate(update: any) { 431 | api.emit('apiUpdate', update) 432 | } 433 | 434 | async function attach() { 435 | setUpdatesProcessor(processUpdateMessage) 436 | const stateResult: UpdatesState = await api('updates.getState', {}, { noErrorBox: true }) 437 | updatesState.seq = stateResult.seq 438 | updatesState.pts = stateResult.pts 439 | updatesState.date = stateResult.date 440 | setTimeout(() => { 441 | updatesState.syncLoading = false 442 | }, 1000) 443 | } 444 | 445 | return { 446 | processUpdateMessage, 447 | addChannelState, 448 | attach 449 | } 450 | } 451 | 452 | export default UpdatesManager -------------------------------------------------------------------------------- /src/tl/index.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import is from 'ramda/src/is' 4 | import has from 'ramda/src/has' 5 | 6 | import { uintToInt, intToUint, bytesToHex, 7 | gzipUncompress, bytesToArrayBuffer, longToInts, lshift32, stringToChars } from '../bin' 8 | 9 | import { WriteMediator, ReadMediator } from './mediator' 10 | import Layout, { getFlags, isSimpleType, getTypeProps } from '../layout' 11 | import { TypeBuffer, TypeWriter, getNakedType, 12 | getString, getTypeConstruct } from './type-buffer' 13 | import type { TLSchema, TLConstruct } from './index.h' 14 | 15 | import Logger from '../util/log' 16 | const debug = Logger`tl` 17 | 18 | const PACKED = 0x3072cfa1 19 | 20 | type SerialConstruct = { 21 | mtproto: boolean, 22 | startMaxLength: number 23 | } 24 | 25 | let apiLayer: Layout 26 | let mtLayer: Layout 27 | 28 | export class Serialization { 29 | writer: TypeWriter = new TypeWriter() 30 | mtproto: boolean 31 | api: TLSchema 32 | mtApi: TLSchema 33 | constructor({ mtproto, startMaxLength }: SerialConstruct, api: TLSchema, mtApi: TLSchema) { 34 | this.api = api 35 | this.mtApi = mtApi 36 | 37 | this.writer.maxLength = startMaxLength 38 | 39 | this.writer.reset() 40 | this.mtproto = mtproto 41 | if (!apiLayer) 42 | apiLayer = new Layout(api) 43 | if (!mtLayer) 44 | mtLayer = new Layout(mtApi) 45 | } 46 | 47 | getBytes(typed?: boolean) { 48 | if (typed) 49 | return this.writer.getBytesTyped() 50 | else 51 | return this.writer.getBytesPlain() 52 | } 53 | 54 | storeMethod(methodName: string, params) { 55 | const layer = this.mtproto 56 | ? mtLayer 57 | : apiLayer 58 | const pred = layer.funcs.get(methodName) 59 | if (!pred) throw new Error(`No method name ${methodName} found`) 60 | 61 | WriteMediator.int(this.writer, 62 | intToUint(`${pred.id}`), 63 | `${methodName}[id]`) 64 | if (pred.hasFlags) { 65 | const flags = getFlags(pred)(params) 66 | this.storeObject(flags, '#', `f ${methodName} #flags ${flags}`) 67 | } 68 | for (const param of pred.params) { 69 | const paramName = param.name 70 | const typeClass = param.typeClass 71 | let fieldObj 72 | if (!has(paramName, params)) { 73 | if (param.isFlag) continue 74 | else if (layer.typeDefaults.has(typeClass)) 75 | fieldObj = layer.typeDefaults.get(typeClass) 76 | else if (isSimpleType(typeClass)) { 77 | switch (typeClass) { 78 | case 'int': fieldObj = 0; break 79 | // case 'long': fieldObj = 0; break 80 | case 'string': fieldObj = ' '; break 81 | // case 'double': fieldObj = 0; break 82 | case 'true': fieldObj = true; break 83 | // case 'bytes': fieldObj = [0]; break 84 | } 85 | } 86 | else throw new Error(`Method ${methodName} did not receive required argument ${paramName}`) 87 | } else { 88 | fieldObj = params[paramName] 89 | } 90 | if (param.isVector) { 91 | if (!Array.isArray(fieldObj)) 92 | throw new TypeError(`Vector argument ${paramName} in ${methodName} required Array,` + 93 | //$FlowIssue 94 | ` got ${fieldObj} ${typeof fieldObj}`) 95 | WriteMediator.int(this.writer, 0x1cb5c415, `${paramName}[id]`) 96 | WriteMediator.int(this.writer, fieldObj.length, `${paramName}[count]`) 97 | for (const [ i, elem ] of fieldObj.entries()) 98 | this.storeObject(elem, param.typeClass, `${paramName}[${i}]`) 99 | } else 100 | this.storeObject(fieldObj, param.typeClass, `f ${methodName}(${paramName})`) 101 | } 102 | /*let condType 103 | let fieldBit 104 | for (const param of methodData.params) { 105 | let type = param.type 106 | if (type.indexOf('?') !== -1) { 107 | condType = type.split('?') 108 | fieldBit = condType[0].split('.') 109 | if (!(params[fieldBit[0]] & 1 << fieldBit[1])) { 110 | continue 111 | } 112 | type = condType[1] 113 | } 114 | const paramName = param.name 115 | const stored = params[paramName] 116 | if (!stored) 117 | stored = this.emptyOfType(type, schema) 118 | if (!stored) 119 | throw new Error(`Method ${methodName}.`+ 120 | ` No value of field ${ param.name } recieved and no Empty of type ${ param.type }`) 121 | this.storeObject(stored, type, `f ${methodName}(${paramName})`) 122 | }*/ 123 | 124 | return pred.returns 125 | } 126 | /*emptyOfType(ofType, schema: TLSchema) { 127 | const resultConstruct = schema.constructors.find( 128 | ({ type, predicate }: TLConstruct) => 129 | type === ofType && 130 | predicate.indexOf('Empty') !== -1) 131 | return resultConstruct 132 | ? { _: resultConstruct.predicate } 133 | : null 134 | }*/ 135 | storeObject(obj, type: string, field: string) { 136 | switch (type) { 137 | case '#': 138 | case 'int': 139 | return WriteMediator.int(this.writer, obj, field) 140 | case 'long': 141 | return WriteMediator.long(this.writer, obj, field) 142 | case 'int128': 143 | return WriteMediator.intBytes(this.writer, obj, 128, field) 144 | case 'int256': 145 | return WriteMediator.intBytes(this.writer, obj, 256, field) 146 | case 'int512': 147 | return WriteMediator.intBytes(this.writer, obj, 512, field) 148 | case 'string': 149 | return WriteMediator.bytes(this.writer, obj, `${field}:string`) 150 | case 'bytes': 151 | return WriteMediator.bytes(this.writer, obj, field) 152 | case 'double': 153 | return WriteMediator.double(this.writer, obj, field) 154 | case 'Bool': 155 | return WriteMediator.bool(this.writer, obj, field) 156 | case 'true': 157 | return 158 | } 159 | 160 | if (Array.isArray(obj)) { 161 | if (type.substr(0, 6) == 'Vector') 162 | WriteMediator.int(this.writer, 0x1cb5c415, `${field}[id]`) 163 | else if (type.substr(0, 6) != 'vector') { 164 | throw new Error(`Invalid vector type ${ type}`) 165 | } 166 | const itemType = type.substr(7, type.length - 8) // for "Vector" 167 | WriteMediator.int(this.writer, obj.length, `${field}[count]`) 168 | for (let i = 0; i < obj.length; i++) { 169 | this.storeObject(obj[i], itemType, `${field }[${ i }]`) 170 | } 171 | return true 172 | } 173 | else if (type.substr(0, 6).toLowerCase() == 'vector') { 174 | throw new Error('Invalid vector object') 175 | } 176 | 177 | if (!is(Object, obj)) 178 | throw new Error(`Invalid object for type ${ type}`) 179 | 180 | const schema = selectSchema(this.mtproto, this.api, this.mtApi) 181 | 182 | const predicate = obj['_'] 183 | let isBare = false 184 | let constructorData = false 185 | isBare = type.charAt(0) == '%' 186 | if (isBare) 187 | type = type.substr(1) 188 | 189 | 190 | for (const tlConst of schema.constructors) { 191 | if (tlConst.predicate == predicate) { 192 | constructorData = tlConst 193 | break 194 | } 195 | } 196 | 197 | if (!constructorData) 198 | throw new Error(`No predicate ${predicate} found`) 199 | 200 | if (predicate == type) 201 | isBare = true 202 | 203 | if (!isBare) 204 | WriteMediator.int(this.writer, 205 | intToUint(constructorData.id), 206 | `${field}.${predicate}[id]`) 207 | 208 | let condType 209 | let fieldBit 210 | 211 | for (const param of constructorData.params) { 212 | type = param.type 213 | if (type.indexOf('?') !== -1) { 214 | condType = type.split('?') 215 | fieldBit = condType[0].split('.') 216 | if (!(obj[fieldBit[0]] & 1 << fieldBit[1])) { 217 | continue 218 | } 219 | type = condType[1] 220 | } 221 | 222 | this.storeObject(obj[param.name], type, `${field}.${ predicate }.${ param.name }`) 223 | } 224 | 225 | return constructorData.type 226 | } 227 | 228 | } 229 | 230 | export class Deserialization { 231 | typeBuffer: TypeBuffer 232 | override: Object 233 | mtproto: boolean 234 | api: TLSchema 235 | mtApi: TLSchema 236 | constructor(buffer: Buffer, { mtproto, override }: DConfig, api: TLSchema, mtApi: TLSchema) { 237 | this.api = api 238 | this.mtApi = mtApi 239 | this.override = override 240 | 241 | this.typeBuffer = new TypeBuffer(buffer) 242 | this.mtproto = mtproto 243 | } 244 | 245 | readInt = (field: string) => { 246 | // log('int')(field, i.toString(16), i) 247 | return ReadMediator.int(this.typeBuffer, field) 248 | } 249 | 250 | fetchInt(field: string = '') { 251 | return this.readInt(`${ field }:int`) 252 | } 253 | 254 | fetchBool(field: string = '') { 255 | const i = this.readInt(`${ field }:bool`) 256 | switch (i) { 257 | case 0x997275b5: return true 258 | case 0xbc799737: return false 259 | default: { 260 | this.typeBuffer.offset -= 4 261 | return this.fetchObject('Object', field) 262 | } 263 | } 264 | } 265 | fetchIntBytes(bits: number, field: string = '') { 266 | if (bits % 32) 267 | throw new Error(`Invalid bits: ${bits}`) 268 | 269 | const len = bits / 8 270 | 271 | const bytes = this.typeBuffer.next(len) 272 | 273 | debug(`int bytes`)(bytesToHex(bytes), `${ field }:int${ bits}`) 274 | 275 | return bytes 276 | } 277 | 278 | fetchRawBytes(len: number | false, field: string = '') { 279 | if (len === false) { 280 | len = this.readInt(`${ field }_length`) 281 | if (len > this.typeBuffer.byteView.byteLength) 282 | throw new Error(`Invalid raw bytes length: ${ len }, buffer len: ${this.typeBuffer.byteView.byteLength}`) 283 | } 284 | const bytes = this.typeBuffer.next(len) 285 | debug(`raw bytes`)(bytesToHex(bytes), field) 286 | 287 | return bytes 288 | } 289 | 290 | fetchPacked(type, field: string = '') { 291 | const compressed = ReadMediator.bytes( this.typeBuffer, `${field}[packed_string]`) 292 | const uncompressed = gzipUncompress(compressed) 293 | const buffer = bytesToArrayBuffer(uncompressed) 294 | const newDeserializer = new Deserialization( 295 | buffer, { 296 | mtproto : this.mtproto, 297 | override: this.override 298 | }, 299 | this.api, this.mtApi) 300 | 301 | return newDeserializer.fetchObject(type, field) 302 | } 303 | 304 | fetchVector(type: string, field: string = '') { 305 | const typeProps = getTypeProps(type) 306 | if (type.charAt(0) === 'V') { 307 | const constructor = this.readInt(`${field}[id]`) 308 | const constructorCmp = uintToInt(constructor) 309 | 310 | if (constructorCmp === PACKED) 311 | return this.fetchPacked(type, field) 312 | if (constructorCmp !== 0x1cb5c415) 313 | throw new Error(`Invalid vector constructor ${constructor}`) 314 | } 315 | const len = this.readInt(`${field}[count]`) 316 | const result = [] 317 | if (len > 0) { 318 | const itemType = type.substr(7, type.length - 8) // for "Vector" 319 | for (let i = 0; i < len; i++) 320 | result.push(this.fetchObject(itemType, `${field}[${i}]`)) 321 | } 322 | 323 | return result 324 | } 325 | 326 | fetchObject(type, field: string = '') { 327 | switch (type) { 328 | case '#': 329 | case 'int': 330 | return this.fetchInt(field) 331 | case 'long': 332 | return ReadMediator.long(this.typeBuffer, field) 333 | case 'int128': 334 | return this.fetchIntBytes(128, field) 335 | case 'int256': 336 | return this.fetchIntBytes(256, field) 337 | case 'int512': 338 | return this.fetchIntBytes(512, field) 339 | case 'string': 340 | return ReadMediator.string(this.typeBuffer, field) 341 | case 'bytes': 342 | return ReadMediator.bytes(this.typeBuffer, field) 343 | case 'double': 344 | return ReadMediator.double(this.typeBuffer, field) 345 | case 'Bool': 346 | return this.fetchBool(field) 347 | case 'true': 348 | return true 349 | } 350 | let fallback 351 | field = field || type || 'Object' 352 | 353 | // const layer = this.mtproto 354 | // ? mtLayer 355 | // : apiLayer 356 | const typeProps = getTypeProps(type) 357 | // layer.typesById 358 | 359 | if (typeProps.isVector) 360 | return this.fetchVector(type, field) 361 | 362 | const schema = selectSchema(this.mtproto, this.api, this.mtApi) 363 | let predicate = false 364 | let constructorData = false 365 | 366 | if (typeProps.isBare) 367 | constructorData = getNakedType(type, schema) 368 | else { 369 | const constructor = this.readInt(`${field}[id]`) 370 | const constructorCmp = uintToInt(constructor) 371 | 372 | if (constructorCmp === PACKED) 373 | return this.fetchPacked(type, field) 374 | 375 | let index = schema.constructorsIndex 376 | if (!index) { 377 | schema.constructorsIndex = index = {} 378 | for (let i = 0; i < schema.constructors.length; i++) 379 | index[schema.constructors[i].id] = i 380 | } 381 | const i = index[constructorCmp] 382 | if (i) 383 | constructorData = schema.constructors[i] 384 | 385 | fallback = false 386 | if (!constructorData && this.mtproto) { 387 | const schemaFallback = this.api 388 | const finded = getTypeConstruct(constructorCmp, schemaFallback) 389 | if (finded) { 390 | constructorData = finded 391 | delete this.mtproto 392 | fallback = true 393 | } 394 | } 395 | if (!constructorData) { 396 | throw new Error(`Constructor not found: ${constructor} ${this.fetchInt()} ${this.fetchInt()}`) 397 | } 398 | } 399 | 400 | predicate = constructorData.predicate 401 | 402 | const result = { '_': predicate } 403 | const overrideKey = (this.mtproto ? 'mt_' : '') + predicate 404 | 405 | if (this.override[overrideKey]) { 406 | this.override[overrideKey].apply(this, [result, `${field}[${predicate}]`]) 407 | } else { 408 | for (const param of constructorData.params) { 409 | type = param.type 410 | // if (type === '#' && isNil(result.pFlags)) 411 | // result.pFlags = {} 412 | if (type.indexOf('?') !== -1) { 413 | const condType = type.split('?') 414 | const fieldBit = condType[0].split('.') 415 | if (!(result[fieldBit[0]] & 1 << fieldBit[1])) 416 | continue 417 | type = condType[1] 418 | } 419 | const paramName = param.name 420 | const value = this.fetchObject(type, `${field}[${predicate}][${paramName}]`) 421 | 422 | result[paramName] = value 423 | } 424 | } 425 | 426 | if (fallback) 427 | this.mtproto = true 428 | 429 | return result 430 | } 431 | 432 | getOffset() { 433 | return this.typeBuffer.offset 434 | } 435 | 436 | fetchEnd() { 437 | if (!this.typeBuffer.isEnd()) 438 | throw new Error('Fetch end with non-empty buffer') 439 | return true 440 | } 441 | 442 | } 443 | 444 | const selectSchema = (mtproto: boolean, api: TLSchema, mtApi: TLSchema) => mtproto 445 | ? mtApi 446 | : api 447 | 448 | type DConfig = { 449 | mtproto: boolean, 450 | override: Object 451 | } 452 | 453 | export type DeserializationFabric = ( 454 | buffer: Buffer, 455 | config?: { 456 | mtproto?: boolean, 457 | override?: Object 458 | }) => Deserialization 459 | 460 | export type SerializationFabric = ( 461 | config?: { 462 | mtproto?: boolean, 463 | startMaxLength?: number 464 | }) => Serialization 465 | 466 | export type TLFabric = { 467 | Serialization: SerializationFabric, 468 | Deserialization: DeserializationFabric 469 | } 470 | 471 | export const TL = (api: TLSchema, mtApi: TLSchema) => ({ 472 | Serialization: ({ mtproto = false, startMaxLength = 2048 /* 2Kb */ } = {}) => 473 | new Serialization({ mtproto, startMaxLength }, api, mtApi), 474 | Deserialization: (buffer: Buffer, { mtproto = false, override = {} }: DConfig = {}) => 475 | new Deserialization(buffer, { mtproto, override }, api, mtApi) 476 | }) 477 | 478 | export * from './mediator' 479 | export { TypeWriter } from './type-buffer' 480 | export default TL 481 | -------------------------------------------------------------------------------- /src/service/authorizer/index.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | import Promise from 'bluebird' 4 | 5 | import blueDefer from '../../util/defer' 6 | import { immediate } from '../../util/smart-timeout' 7 | import CryptoWorker from '../../crypto' 8 | 9 | import random from '../secure-random' 10 | import { applyServerTime, dTime, tsNow } from '../time-manager' 11 | 12 | import { bytesCmp, bytesToHex, sha1BytesSync, nextRandomInt, 13 | aesEncryptSync, rsaEncrypt, aesDecryptSync, bytesToArrayBuffer, 14 | bytesFromHex, bytesXor } from '../../bin' 15 | import { bpe, str2bigInt, one, 16 | dup, sub_, sub, greater } from '../../vendor/leemon' 17 | 18 | import Logger from '../../util/log' 19 | 20 | const log = Logger`auth` 21 | 22 | // import { ErrorBadResponse } from '../../error' 23 | 24 | import SendPlainReq from './send-plain-req' 25 | 26 | import type { TLFabric } from '../../tl' 27 | 28 | const primeHex = 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d93' + 29 | '0f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f6945' + 30 | '8705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4a' + 31 | 'c8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba7' + 32 | '4d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9c' + 33 | 'a3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851' + 34 | 'f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b' 35 | 36 | const concat = (e1, e2) => [...e1, ...e2] 37 | 38 | const tmpAesKey = (serverNonce, newNonce) => { 39 | const arr1 = concat(newNonce, serverNonce) 40 | const arr2 = concat(serverNonce, newNonce) 41 | const key1 = sha1BytesSync(arr1) 42 | const key2 = sha1BytesSync(arr2).slice(0, 12) 43 | return key1.concat(key2) 44 | } 45 | 46 | const tmpAesIv = (serverNonce, newNonce) => { 47 | const arr1 = concat(serverNonce, newNonce) 48 | const arr2 = concat(newNonce, newNonce) 49 | const arr3 = newNonce.slice(0, 4) 50 | const key1 = sha1BytesSync(arr1) 51 | const key2 = sha1BytesSync(arr2) 52 | return key1.slice(12).concat(key2, arr3) 53 | } 54 | 55 | type Defer = { 56 | resolve: (res: any) => void, 57 | reject: (res: any) => void, 58 | promise: Promise 59 | } 60 | 61 | type Cached = {[id: number]: Defer} 62 | 63 | export type Args = { 64 | select: () => Promise, 65 | prepare: () => Promise 66 | } 67 | 68 | type Bytes = number[] 69 | 70 | type AuthBasic = { 71 | dcID: number, 72 | dcUrl: string, 73 | nonce: Bytes, 74 | deferred: Defer, 75 | serverNonce: Bytes, 76 | pq: Bytes, 77 | fingerprints: Bytes, 78 | p: number, 79 | q: number, 80 | publicKey: { 81 | fingerprint: string 82 | }, 83 | newNonce: number[], 84 | b: Bytes, 85 | g: number, 86 | gA: any, 87 | retry: number, 88 | dhPrime: any, 89 | serverTime: number, 90 | localTime: number, 91 | tmpAesKey: Bytes, 92 | tmpAesIv: Bytes, 93 | authKeyID: Bytes, 94 | authKey: string, 95 | serverSalt: Bytes 96 | } 97 | 98 | const minSize = Math.ceil(64 / bpe) + 1 99 | 100 | const getTwoPow = () => { //Dirty hack to count 2^(2048 - 64) 101 | //This number contains 496 zeroes in hex 102 | const arr = Array(496) 103 | .fill('0') 104 | arr.unshift('1') 105 | const hex = arr.join('') 106 | const res = str2bigInt(hex, 16, minSize) 107 | return res 108 | } 109 | 110 | const leemonTwoPow = getTwoPow() 111 | 112 | export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, prepare }: Args) => { 113 | const sendPlainReq = SendPlainReq({ Serialization, Deserialization }) 114 | 115 | async function mtpSendReqPQ(auth: AuthBasic) { 116 | const deferred = auth.deferred 117 | log('Send req_pq')(bytesToHex(auth.nonce)) 118 | 119 | const request = Serialization({ mtproto: true }) 120 | const reqBox = request.writer 121 | request.storeMethod('req_pq', { nonce: auth.nonce }) 122 | 123 | 124 | let deserializer 125 | try { 126 | await prepare() 127 | deserializer = await sendPlainReq(auth.dcUrl, reqBox.getBuffer()) 128 | } catch (err) { 129 | console.error(dTime(), 'req_pq error', err.message) 130 | deferred.reject(err) 131 | throw err 132 | } 133 | 134 | try { 135 | const response = deserializer.fetchObject('ResPQ', 'ResPQ') 136 | 137 | if (response._ !== 'resPQ') { 138 | const error = new Error(`[MT] resPQ response invalid: ${ response._}`) 139 | deferred.reject(error) 140 | return Promise.reject(error) 141 | } 142 | if (!bytesCmp(auth.nonce, response.nonce)) { 143 | const error = new Error('[MT] resPQ nonce mismatch') 144 | deferred.reject(error) 145 | return Promise.reject(error) 146 | } 147 | auth.serverNonce = response.server_nonce 148 | auth.pq = response.pq 149 | auth.fingerprints = response.server_public_key_fingerprints 150 | 151 | log('Got ResPQ')(bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints) 152 | 153 | const key = await select(auth.fingerprints) 154 | 155 | if (key) 156 | auth.publicKey = key 157 | else { 158 | const error = new Error('[MT] No public key found') 159 | deferred.reject(error) 160 | return Promise.reject(error) 161 | } 162 | log('PQ factorization start')(auth.pq) 163 | const [ p, q, it ] = await CryptoWorker.factorize(auth.pq) 164 | 165 | auth.p = p 166 | auth.q = q 167 | log('PQ factorization done')(it) 168 | } catch (error) { 169 | log('Worker error')(error, error.stack) 170 | deferred.reject(error) 171 | throw error 172 | } 173 | 174 | 175 | return auth 176 | } 177 | 178 | async function mtpSendReqDhParams(auth: AuthBasic) { 179 | const deferred = auth.deferred 180 | 181 | auth.newNonce = new Array(32) 182 | random(auth.newNonce) 183 | 184 | const data = Serialization({ mtproto: true }) 185 | const dataBox = data.writer 186 | data.storeObject({ 187 | _ : 'p_q_inner_data', 188 | pq : auth.pq, 189 | p : auth.p, 190 | q : auth.q, 191 | nonce : auth.nonce, 192 | server_nonce: auth.serverNonce, 193 | new_nonce : auth.newNonce 194 | }, 'P_Q_inner_data', 'DECRYPTED_DATA') 195 | 196 | const dataWithHash = sha1BytesSync(dataBox.getBuffer()).concat(data.getBytes()) 197 | 198 | const request = Serialization({ mtproto: true }) 199 | const reqBox = request.writer 200 | request.storeMethod('req_DH_params', { 201 | nonce : auth.nonce, 202 | server_nonce : auth.serverNonce, 203 | p : auth.p, 204 | q : auth.q, 205 | public_key_fingerprint: auth.publicKey.fingerprint, 206 | encrypted_data : rsaEncrypt(auth.publicKey, dataWithHash) 207 | }) 208 | 209 | 210 | log('afterReqDH')('Send req_DH_params') 211 | 212 | let deserializer 213 | try { 214 | deserializer = await sendPlainReq(auth.dcUrl, reqBox.getBuffer()) 215 | } catch (error) { 216 | deferred.reject(error) 217 | throw error 218 | } 219 | 220 | 221 | const response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE') 222 | 223 | if (response._ !== 'server_DH_params_fail' && response._ !== 'server_DH_params_ok') { 224 | const error = new Error(`[MT] Server_DH_Params response invalid: ${ response._}`) 225 | deferred.reject(error) 226 | throw error 227 | } 228 | 229 | if (!bytesCmp(auth.nonce, response.nonce)) { 230 | const error = new Error('[MT] Server_DH_Params nonce mismatch') 231 | deferred.reject(error) 232 | throw error 233 | } 234 | 235 | if (!bytesCmp(auth.serverNonce, response.server_nonce)) { 236 | const error = new Error('[MT] Server_DH_Params server_nonce mismatch') 237 | deferred.reject(error) 238 | throw error 239 | } 240 | 241 | if (response._ === 'server_DH_params_fail') { 242 | const newNonceHash = sha1BytesSync(auth.newNonce).slice(-16) 243 | if (!bytesCmp(newNonceHash, response.new_nonce_hash)) { 244 | const error = new Error('[MT] server_DH_params_fail new_nonce_hash mismatch') 245 | deferred.reject(error) 246 | throw error 247 | } 248 | const error = new Error('[MT] server_DH_params_fail') 249 | deferred.reject(error) 250 | throw error 251 | } 252 | 253 | // try { 254 | mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer) 255 | // } catch (e) { 256 | // deferred.reject(e) 257 | // return false 258 | // } 259 | 260 | return auth 261 | } 262 | 263 | function mtpDecryptServerDhDataAnswer(auth: AuthBasic, encryptedAnswer) { 264 | auth.tmpAesKey = tmpAesKey(auth.serverNonce, auth.newNonce) 265 | auth.tmpAesIv = tmpAesIv(auth.serverNonce, auth.newNonce) 266 | 267 | const answerWithHash = aesDecryptSync( 268 | encryptedAnswer, 269 | auth.tmpAesKey, 270 | auth.tmpAesIv) 271 | 272 | const hash = answerWithHash.slice(0, 20) 273 | const answerWithPadding = answerWithHash.slice(20) 274 | const buffer = bytesToArrayBuffer(answerWithPadding) 275 | 276 | const deserializer = Deserialization(buffer, { mtproto: true }) 277 | const response = deserializer.fetchObject('Server_DH_inner_data', 'server_dh') 278 | 279 | if (response._ !== 'server_DH_inner_data') 280 | throw new Error(`[MT] server_DH_inner_data response invalid`) 281 | 282 | if (!bytesCmp(auth.nonce, response.nonce)) 283 | throw new Error('[MT] server_DH_inner_data nonce mismatch') 284 | 285 | if (!bytesCmp(auth.serverNonce, response.server_nonce)) 286 | throw new Error('[MT] server_DH_inner_data serverNonce mismatch') 287 | 288 | log('DecryptServerDhDataAnswer')('Done decrypting answer') 289 | auth.g = response.g 290 | auth.dhPrime = response.dh_prime 291 | auth.gA = response.g_a 292 | auth.serverTime = response.server_time 293 | auth.retry = 0 294 | 295 | mtpVerifyDhParams(auth.g, auth.dhPrime, auth.gA) 296 | 297 | const offset = deserializer.getOffset() 298 | 299 | if (!bytesCmp(hash, sha1BytesSync(answerWithPadding.slice(0, offset)))) 300 | throw new Error('[MT] server_DH_inner_data SHA1-hash mismatch') 301 | 302 | auth.localTime = tsNow() 303 | applyServerTime(auth.serverTime, auth.localTime) 304 | } 305 | 306 | function mtpVerifyDhParams(g, dhPrime, gA) { 307 | const innerLog = log('VerifyDhParams') 308 | innerLog('begin') 309 | const dhPrimeHex = bytesToHex(dhPrime) 310 | if (g !== 3 || dhPrimeHex !== primeHex) 311 | // The verified value is from https://core.telegram.org/mtproto/security_guidelines 312 | throw new Error('[MT] DH params are not verified: unknown dhPrime') 313 | innerLog('dhPrime cmp OK') 314 | 315 | // const gABigInt = new BigInteger(bytesToHex(gA), 16) 316 | // const dhPrimeBigInt = new BigInteger(dhPrimeHex, 16) 317 | 318 | const dhPrimeLeemon = str2bigInt(dhPrimeHex, 16, minSize) 319 | const gALeemon = str2bigInt(bytesToHex(gA), 16, minSize) 320 | const dhDec = dup(dhPrimeLeemon) 321 | sub_(dhDec, one) 322 | // const dhDecStr = bigInt2str(dhDec, 16) 323 | // const comp = dhPrimeBigInt.subtract(BigInteger.ONE).toString(16) 324 | // console.log(dhPrimeLeemon, dhDecStr === comp) 325 | const case1 = !greater(gALeemon, one) 326 | //gABigInt.compareTo(BigInteger.ONE) <= 0 327 | const case2 = !greater(dhDec, gALeemon) 328 | //gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)) >= 0 329 | if (case1) 330 | throw new Error('[MT] DH params are not verified: gA <= 1') 331 | 332 | if (case2) 333 | throw new Error('[MT] DH params are not verified: gA >= dhPrime - 1') 334 | // console.log(dTime(), '1 < gA < dhPrime-1 OK') 335 | 336 | 337 | // const two = new BigInteger(null) 338 | // two.fromInt(2) 339 | // const twoPow = two.pow(2048 - 64) 340 | 341 | const case3 = !!greater(leemonTwoPow, gALeemon) 342 | //gABigInt.compareTo(twoPow) < 0 343 | const dhSubPow = dup(dhPrimeLeemon) 344 | sub(dhSubPow, leemonTwoPow) 345 | const case4 = !greater(dhSubPow, gALeemon) 346 | //gABigInt.compareTo(dhPrimeBigInt.subtract(twoPow)) >= 0 347 | // console.log(case3 === gABigInt.compareTo(twoPow) < 0) 348 | if (case3) 349 | throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}') 350 | if (case4) 351 | throw new Error('[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}') 352 | innerLog('2^{2048-64} < gA < dhPrime-2^{2048-64} OK') 353 | 354 | return true 355 | } 356 | 357 | async function mtpSendSetClientDhParams(auth: AuthBasic) { 358 | const deferred = auth.deferred 359 | const gBytes = bytesFromHex(auth.g.toString(16)) 360 | 361 | auth.b = new Array(256) 362 | random(auth.b) 363 | 364 | const gB = await CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime) 365 | const data = Serialization({ mtproto: true }) 366 | 367 | data.storeObject({ 368 | _ : 'client_DH_inner_data', 369 | nonce : auth.nonce, 370 | server_nonce: auth.serverNonce, 371 | retry_id : [0, auth.retry++], 372 | g_b : gB 373 | }, 'Client_DH_Inner_Data', 'client_DH') 374 | 375 | const dataWithHash = sha1BytesSync(data.writer.getBuffer()).concat(data.getBytes()) 376 | 377 | const encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv) 378 | 379 | const request = Serialization({ mtproto: true }) 380 | 381 | request.storeMethod('set_client_DH_params', { 382 | nonce : auth.nonce, 383 | server_nonce : auth.serverNonce, 384 | encrypted_data: encryptedData 385 | }) 386 | 387 | log('onGb')('Send set_client_DH_params') 388 | 389 | const deserializer = await sendPlainReq(auth.dcUrl, request.writer.getBuffer()) 390 | 391 | const response = deserializer.fetchObject('Set_client_DH_params_answer', 'client_dh') 392 | 393 | if (response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') { 394 | const error = new Error(`[MT] Set_client_DH_params_answer response invalid: ${ response._}`) 395 | deferred.reject(error) 396 | throw error 397 | } 398 | 399 | if (!bytesCmp(auth.nonce, response.nonce)) { 400 | const error = new Error('[MT] Set_client_DH_params_answer nonce mismatch') 401 | deferred.reject(error) 402 | throw error 403 | } 404 | 405 | if (!bytesCmp(auth.serverNonce, response.server_nonce)) { 406 | const error = new Error('[MT] Set_client_DH_params_answer server_nonce mismatch') 407 | deferred.reject(error) 408 | throw error 409 | } 410 | 411 | const authKey = await CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime) 412 | 413 | const authKeyHash = sha1BytesSync(authKey), 414 | authKeyAux = authKeyHash.slice(0, 8), 415 | authKeyID = authKeyHash.slice(-8) 416 | 417 | log('Got Set_client_DH_params_answer')(response._) 418 | switch (response._) { 419 | case 'dh_gen_ok': { 420 | const newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16) 421 | 422 | if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) { 423 | deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch')) 424 | return false 425 | } 426 | 427 | const serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8)) 428 | // console.log('Auth successfull!', authKeyID, authKey, serverSalt) 429 | 430 | auth.authKeyID = authKeyID 431 | auth.authKey = authKey 432 | auth.serverSalt = serverSalt 433 | 434 | deferred.resolve(auth) 435 | break 436 | } 437 | case 'dh_gen_retry': { 438 | const newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16) 439 | if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) { 440 | deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch')) 441 | return false 442 | } 443 | 444 | return mtpSendSetClientDhParams(auth) 445 | } 446 | case 'dh_gen_fail': { 447 | const newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16) 448 | if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) { 449 | deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch')) 450 | return false 451 | } 452 | 453 | deferred.reject(new Error('[MT] Set_client_DH_params_answer fail')) 454 | return false 455 | } 456 | } 457 | } 458 | 459 | const authChain = (auth: AuthBasic) => 460 | mtpSendReqPQ(auth) 461 | .then(mtpSendReqDhParams) 462 | .then(mtpSendSetClientDhParams) 463 | 464 | function mtpAuth(dcID: number, cached: Cached, dcUrl: string) { 465 | if (cached[dcID]) 466 | return cached[dcID].promise 467 | log('mtpAuth', 'dcID', 'dcUrl')(dcID, dcUrl) 468 | const nonce = [] 469 | for (let i = 0; i < 16; i++) 470 | nonce.push(nextRandomInt(0xFF)) 471 | 472 | if (!dcUrl) 473 | return Promise.reject( 474 | new Error(`[MT] No server found for dc ${dcID} url ${dcUrl}`)) 475 | 476 | const auth: any = { 477 | dcID, 478 | dcUrl, 479 | nonce, 480 | deferred: blueDefer() 481 | } 482 | 483 | immediate(authChain, auth) 484 | 485 | cached[dcID] = auth.deferred 486 | 487 | cached[dcID].promise.catch(() => { 488 | delete cached[dcID] 489 | }) 490 | 491 | return cached[dcID].promise 492 | } 493 | 494 | return mtpAuth 495 | } 496 | export default Auth --------------------------------------------------------------------------------