diff --git a/.babelrc b/.babelrc index 84e5f58..362aec7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,20 +1,19 @@ { "presets": [ - // ["es2015", { "modules": false }] ], "plugins": [ "transform-flow-strip-types", - // ["fast-async", { - // "useRuntimeModule":true - // }], + "transform-exponentiation-operator", + ["transform-es2015-for-of", { + "loose": true + }], "transform-class-properties", "transform-async-to-generator", - // ["transform-async-to-module-method", { - // "module": "bluebird", - // "method": "coroutine" - // }], - ["transform-object-rest-spread", { "useBuiltIns": true }], - "closure-elimination" + ["transform-es2015-block-scoping", { + "throwIfClosureRequired": true + }], + ["transform-object-rest-spread", { "useBuiltIns": true }] + ,"closure-elimination" ], "env": { "commonjs": { diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..153dadb --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +lib/** +es/** +dist/** diff --git a/.flowconfig b/.flowconfig index 83de378..dfedf54 100644 --- a/.flowconfig +++ b/.flowconfig @@ -13,4 +13,5 @@ [options] suppress_comment=\\(.\\|\n\\)*\\$FlowIssue -suppress_type=$FlowIssue \ No newline at end of file +suppress_type=$FlowIssue +unsafe.enable_getters_and_setters=true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e8d6564 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# 2.2.2 +* Fixed bug with cyrillic text encoding +* Fixed bug with several broken methods invocations +* Optimized authorization performance +* Added fix for automatic base data center selection by [@goodmind][] + +# 2.2.1 +* Flag (optional type) fields now acts like common fields. +* Zeroes, empty strings and empty types now can be omited. write only useful fields. +* *invokeWithLayer* api field now may detects internally and don't required (but still valid). +* Type check argument fields +* Fix auth race condition +* Add batch async logger + +# 2.2.0 + +* **breaking** Instance now creates without `new` +* **breaking** Rename module exports from `ApiManager` to `MTProto` + +# =<2.1.0 + +Several early alpha versions based on new architechture + +--- + +# 1.1.0 *(beta)* + +* **breaking** Remove all functions from response. Just use the field values. +* Remove logger from response +* Add changelog.md + +# 1.0.6 *(beta)* + +* Https connection. Usage: +```javascript +const { network } = require('telegram-mtproto') +const connection = network.http({ host: 'ip', port: '80', protocol: 'https' }) +``` +* Websockets connection. Usage: +```javascript +const connection = network.wc({ host: 'ip', port: '80' }) +``` +* Precision timing +* Major performance boost + +[@goodmind]: https://github.com/goodmind/ \ No newline at end of file diff --git a/README.md b/README.md index c2c0dba..e095d79 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,6 @@ const phone = { } const api = { - invokeWithLayer: 0xda9b0d0d, layer : 57, initConnection : 0x69796de9, api_id : 49631 diff --git a/examples/chat-history.js b/examples/chat-history.js index b631d55..9e6da64 100644 --- a/examples/chat-history.js +++ b/examples/chat-history.js @@ -1,13 +1,11 @@ -const { pluck, last } = require('ramda') +const { pluck } = require('ramda') const { inputField } = require('./fixtures') const telegram = require('./init') const getChat = async () => { const dialogs = await telegram('messages.getDialogs', { - offset : 0, - limit : 50, - offset_peer: { _: 'inputPeerEmpty' } + limit: 50, }) const { chats } = dialogs const selectedChat = await selectChat(chats) @@ -70,7 +68,17 @@ const printMessages = messages => { return formatted } + +const searchUsers = async (username) => { + const results = await telegram('contacts.search', { + q : username, + limit: 100, + }) + return results +} + module.exports = { getChat, - chatHistory + chatHistory, + searchUsers } \ No newline at end of file diff --git a/examples/from-readme.js b/examples/from-readme.js new file mode 100644 index 0000000..3f1fa85 --- /dev/null +++ b/examples/from-readme.js @@ -0,0 +1,36 @@ +const { MTProto } = require('../lib') + +const phone = { + num : '+9996620001', + code: '22222' +} + +const api = { + layer : 57, + initConnection: 0x69796de9, + api_id : 49631 +} + +const server = { + dev: true //We will connect to the test server. +} //Any empty configurations fields can just not be specified + +const client = MTProto({ server, api }) + +async function connect(){ + const { phone_code_hash } = await client('auth.sendCode', { + phone_number : phone.num, + current_number: false, + api_id : 49631, + api_hash : 'fb050b8f6771e15bfda5df2409931569' + }) + const { user } = await client('auth.signIn', { + phone_number: phone.num, + phone_code_hash, + phone_code : phone.code + }) + + console.log('signed as ', user) +} + +connect() \ No newline at end of file diff --git a/examples/index.js b/examples/index.js index e15dd2f..26f25ba 100644 --- a/examples/index.js +++ b/examples/index.js @@ -1,10 +1,13 @@ const login = require('./login') -const { getChat, chatHistory } = require('./chat-history') +const { getChat, chatHistory, searchUsers } = require('./chat-history') +const updateProfile = require('./update-profile') const run = async () => { - await login() - const chat = await getChat() - await chatHistory(chat) + const first_name = await login() + const res = await searchUsers() + await updateProfile(first_name) + // const chat = await getChat() + // await chatHistory(chat) } run() \ No newline at end of file diff --git a/examples/login.js b/examples/login.js index 7637cec..4331328 100644 --- a/examples/login.js +++ b/examples/login.js @@ -34,7 +34,7 @@ const login = async () => { username = '' } = user console.log('signIn', first_name, username, user.phone) - return telegram + return first_name } catch (error) { console.error(error) } diff --git a/examples/update-profile.js b/examples/update-profile.js new file mode 100644 index 0000000..ec4c80f --- /dev/null +++ b/examples/update-profile.js @@ -0,0 +1,11 @@ +const telegram = require('./init') + +const updateProfile = async (currentName) => { + const result = await telegram('account.updateProfile', { + first_name: 'lam'//currentName + 'test' + }) + console.log('updateProfile', result) + return result +} + +module.exports = updateProfile \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 9b9cb0e..820d8cf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -38,6 +38,7 @@ declare module 'telegram-mtproto' { interface ApiManagerInstance { readonly storage: AsyncStorage + readonly updates: any (method: string): Promise (method: string, params: Object): Promise (method: string, params: Object, options: Object): Promise @@ -48,16 +49,19 @@ declare module 'telegram-mtproto' { new (): ApiManagerInstance new ({ server, api, app, schema, mtSchema }: Config): ApiManagerInstance } - export var ApiManager: IApiManager + export const ApiManager: IApiManager class ApiManagerClass { readonly storage: AsyncStorage setUserAuth(dc: number, userAuth: T): void on(event: string|string[], handler: Function) } export interface AsyncStorage { - get(...keys: string[]): Promise - set(obj: Object): Promise + get(key: string): Promise + set(key: string, val: any): Promise remove(...keys: string[]): Promise clear(): Promise<{}> } + + function MTProto({ server, api, app, schema, mtSchema }: Config): ApiManagerInstance + export default MTProto } \ No newline at end of file diff --git a/package.json b/package.json index 008c377..ec6a902 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,11 @@ "ajv": "^4.11.5", "ajv-keywords": "^1.5.1", "axios": "^0.15.3", - "bluebird": "^3.4.7", + "bluebird": "^3.5.0", "debug": "^2.6.3", "detect-node": "^2.0.3", "eventemitter2": "^3.0.2", + "memoizee": "^0.4.4", "pako": "^1.0.4", "ramda": "^0.23.0", "randombytes": "^2.0.3", @@ -41,23 +42,26 @@ }, "devDependencies": { "@types/debug": "0.0.29", + "@types/memoizee": "^0.4.0", "babel-cli": "^6.24.0", "babel-core": "^6.24.0", - "babel-eslint": "^7.1.1", - "babel-loader": "^6.4.0", + "babel-eslint": "^7.2.0", + "babel-loader": "^6.4.1", "babel-plugin-closure-elimination": "^1.1.14", "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-async-to-module-method": "^6.22.0", "babel-plugin-transform-class-properties": "^6.23.0", + "babel-plugin-transform-es2015-block-scoping": "^6.23.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", + "babel-plugin-transform-exponentiation-operator": "^6.22.0", "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-plugin-transform-object-rest-spread": "^6.23.0", "cross-env": "^3.2.4", - "eslint": "^3.17.1", + "eslint": "^3.18.0", "eslint-plugin-babel": "^4.1.1", - "eslint-plugin-flowtype": "^2.30.3", + "eslint-plugin-flowtype": "^2.30.4", "eslint-plugin-promise": "^3.5.0", - "flow-bin": "^0.41.0", + "flow-bin": "^0.42.0", "hirestime": "^3.1.0", "nightmare": "^2.10.0", "prompt": "^1.0.0", @@ -99,5 +103,5 @@ "type": "git", "url": "git+https://github.com/zerobias/telegram-mtproto.git" }, - "version": "2.2.0" + "version": "2.2.2" } diff --git a/src/bin.js b/src/bin.js index 9c24f73..c0cfc9e 100644 --- a/src/bin.js +++ b/src/bin.js @@ -14,6 +14,16 @@ import { eGCD_, greater, divide_, str2bigInt, equalsInt, const rushaInstance = new Rusha(1024 * 1024) + + +export function stringToChars(str: string) { + const ln = str.length + const result: number[] = Array(ln) + for (let i = 0; i < ln; ++i) + result[i] = str.charCodeAt(i) + return result +} + export const strDecToHex = str => toLower( bigInt2str( str2bigInt(str, 10, 0), 16 @@ -27,7 +37,7 @@ export function bytesToHex(bytes = []) { return arr.join('') } -export function bytesFromHex(hexString) { +export function bytesFromHex(hexString: string) { const len = hexString.length let start = 0 const bytes = [] @@ -163,7 +173,7 @@ const dividerLem = str2bigInt('100000000', 16, 4) // () => console.log(`Timer L ${timeL} B ${timeB}`, ...a, ...b, n || ''), // 100) -export function longToInts(sLong) { +export function longToInts(sLong: string) { const lemNum = str2bigInt(sLong, 10, 6) const div = new Array(lemNum.length) const rem = new Array(lemNum.length) @@ -195,23 +205,16 @@ export const rshift32 = str => { return bigInt2str(num, 10) } -// export function longFromLem(high, low) { -// const highNum = int2bigInt(high, 96, 0) -// leftShift_(highNum, 32) - -// addInt_(highNum, low) -// const res = bigInt2str(highNum, 10) -// return res -// } - -export function intToUint(val) { - val = parseInt(val) //TODO PERF parseInt is a perfomance issue - if (val < 0) - val = val + 0x100000000 - return val +export function intToUint(val: string) { + let result = ~~val + if (result < 0) + result = result + 0x100000000 + return result } + const middle = 0x100000000 / 2 - 1 -export function uintToInt(val) { + +export function uintToInt(val: number): number { if (val > middle) val = val - 0x100000000 return val diff --git a/src/error.js b/src/error.js index f834f0d..46e10c7 100644 --- a/src/error.js +++ b/src/error.js @@ -1,6 +1,6 @@ //@flow -import type { TypeBuffer } from './tl/types' +import type { TypeBuffer } from './tl/type-buffer' export class MTError extends Error { static getMessage(code: number, type: string, message: string) { diff --git a/src/index.js b/src/index.js index c6e01dd..f6db04b 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import MTProto from './service/main/wrap' export { CryptoWorker } from './crypto' export { bin } from './bin' export { ApiManager } from './service/api-manager/index' +export { setLogger } from './util/log' import * as MtpTimeManager from './service/time-manager' export { MtpTimeManager } diff --git a/src/layout/index.h.js b/src/layout/index.h.js new file mode 100644 index 0000000..4d8e336 --- /dev/null +++ b/src/layout/index.h.js @@ -0,0 +1,22 @@ +//@flow + +export type { TLMethod, TLConstruct, TLSchema } from '../tl/index.h' + +export type SchemaParam = { + name: string, + type: string +} + +export type SchemaElement = { + id: string, + type: string, + params: SchemaParam[] +} + +export type TLParam = { + name: string, + typeClass: string, + isVector: boolean, + isFlag: boolean, + flagIndex: number +} \ No newline at end of file diff --git a/src/layout/index.js b/src/layout/index.js new file mode 100644 index 0000000..fbb73e3 --- /dev/null +++ b/src/layout/index.js @@ -0,0 +1,235 @@ +//@flow + +import has from 'ramda/src/has' +import flip from 'ramda/src/flip' +import contains from 'ramda/src/contains' + +import type { TLParam, SchemaElement, TLMethod, TLConstruct, TLSchema, SchemaParam } from './index.h' + +class TypeClass { + name: string + + types: Set = new Set + constructor(name: string) { + this.name = name + } + isTypeOf(value: any): boolean { + return false + } +} + +class Argument { + id: string + name: string + typeClass: string + isVector: boolean + isBare: boolean + isFlag: boolean + flagIndex: number + fullType: string + constructor(id: string, + name: string, + typeClass: string, + isVector: boolean = false, + isBare: boolean = false, + isFlag: boolean = false, + flagIndex: number = NaN) { + this.name = name + this.typeClass = typeClass + this.isVector = isVector + this.isBare = isBare + this.isFlag = isFlag + this.flagIndex = flagIndex + this.id = id + + this.fullType = Argument.fullType(this) + } + static fullType(obj: Argument) { + const { typeClass, isVector, isFlag, flagIndex } = obj + let result = typeClass + if (isVector) + result = `Vector<${result}>` + if (isFlag) + result = `flags.${flagIndex}?${result}` + return result + } +} + +class Creator { + id: number + name: string //predicate or method + hasFlags: boolean + params: Argument[] + constructor(id: number, + name: string, + hasFlags: boolean, + params: Argument[]) { + this.id = id + this.name = name + this.hasFlags = hasFlags + this.params = params + } +} + +class Method extends Creator { + returns: string + constructor(id: number, + name: string, + hasFlags: boolean, + params: Argument[], + returns: string) { + super(id, name, hasFlags, params) + this.returns = returns + } +} + +class Type extends Creator { + typeClass: string + constructor(id: number, + name: string, + hasFlags: boolean, + params: Argument[], + typeClass: string) { + super(id, name, hasFlags, params) + this.typeClass = typeClass + } +} + +const isFlagItself = + (param: SchemaParam) => + param.name === 'flags' && + param.type === '#' + +export class Layout { + typeClasses: Map = new Map + creators: Set = new Set + args: Map = new Map + funcs: Map = new Map + types: Map = new Map + typesById: Map = new Map + typeDefaults: Map = new Map + schema: TLSchema + makeCreator(elem: SchemaElement, + name: string, + sign: string, + Construct: typeof Method | typeof Type) { + const args: Argument[] = [] + let hasFlags = false + for (const [ i, param ] of elem.params.entries()) { + if (isFlagItself(param)) { + hasFlags = true + continue + } + const id = `${name}.${param.name}/${i}` + const { typeClass, isVector, isFlag, flagIndex, isBare } = getTypeProps(param.type) + if (isFlag) hasFlags = true + this.pushTypeClass(typeClass) + const arg = new Argument(id, param.name, typeClass, isVector, isBare, isFlag, flagIndex) + args.push(arg) + this.args.set(id, arg) + } + const id = parseInt(elem.id, 10) + const creator = new Construct(id, name, hasFlags, args, sign) + this.creators.add(name) + if (creator instanceof Method) + this.funcs.set(name, creator) + else if (creator instanceof Type) { + this.types.set(name, creator) + this.typesById.set(id, creator) + } + } + makeMethod(elem: TLMethod) { + const name = elem.method + const returns = elem.type + this.pushTypeClass(returns, name) + this.makeCreator(elem, name, returns, Method) + } + pushTypeClass(typeClass: string, type?: string) { + let instance + if (this.typeClasses.has(typeClass)) + instance = this.typeClasses.get(typeClass) + else { + instance = new TypeClass(typeClass) + this.typeClasses.set(typeClass, instance) + } + if (type && instance) + instance.types.add(type) + } + makeType(elem: TLConstruct) { + const name = elem.predicate + const typeClass = elem.type + this.pushTypeClass(typeClass, name) + this.makeCreator(elem, name, typeClass, Type) + } + makeLayout(schema: TLSchema) { + const { methods, constructors } = schema + constructors.map(this.makeType) + methods.map(this.makeMethod) + for (const [ key, type ] of this.types.entries()) + if (hasEmpty(key)) + this.typeDefaults.set(type.typeClass, { _: key }) + } + constructor(schema: TLSchema) { + //$FlowIssue + this.makeType = this.makeType.bind(this) + //$FlowIssue + this.makeMethod = this.makeMethod.bind(this) + // this.schema = schema + this.makeLayout(schema) + } +} +const hasEmpty = contains('Empty') +const hasQuestion = contains('?') +const hasVector = contains('<') +const hasBare = contains('%') + +export const getTypeProps = (rawType: string) => { + const result = { + typeClass: rawType, + isVector : false, + isFlag : false, + flagIndex: NaN, + isBare : false + } + if (hasQuestion(rawType)) { + const [ prefix, rest ] = rawType.split('?') + const [ , index ] = prefix.split('.') + result.isFlag = true + result.flagIndex = +index + result.typeClass = rest + } + if (hasVector(result.typeClass)) { + result.isVector = true + result.typeClass = result.typeClass.slice(7, -1) + } + if (hasBare(result.typeClass)) { + result.isBare = true + result.typeClass = result.typeClass.slice(1) + } + return result +} + +export const isSimpleType: (type: string) => boolean = + flip(contains)( + ['int', /*'long',*/ 'string', /*'double', */'true', /*'bytes'*/]) + +const getFlagsRed = + (data: Object) => + (acc: number, { name, flagIndex }: Argument) => + has(name, data) + ? acc + 2 ** flagIndex + : acc + +export const getFlags = ({ params }: Creator) => { + const flagsParams = + params.filter( + (e: Argument) => e.isFlag) + + return (data: Object) => + flagsParams + .reduce( + getFlagsRed(data), + 0) +} + +export default Layout \ No newline at end of file diff --git a/src/service/api-manager/index.h.js b/src/service/api-manager/index.h.js index 6efd34c..28c8420 100644 --- a/src/service/api-manager/index.h.js +++ b/src/service/api-manager/index.h.js @@ -1,5 +1,7 @@ //@flow +import type { Emit, On } from '../main/index.h' + export type Bytes = number[] export type PublicKey = { //TODO remove this @@ -23,7 +25,7 @@ export type AsyncStorage = { //TODO remove this noPrefix(): void } -type Cached = { +export type Cached = { [id: number]: Model } @@ -45,5 +47,6 @@ export type ApiManagerInstance = { (method: string, params: Object, options: Object): Promise, storage: AsyncStorage, setUserAuth(dc: number, userAuth: any): void, - on: (event: string | string[], handler: Function) => void + on: On, + emit: Emit } diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index 2348bd3..ec1e4cb 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -1,6 +1,7 @@ //@flow import Promise from 'bluebird' +// import UpdatesManager from '../updates' import isNil from 'ramda/src/isNil' import is from 'ramda/src/is' @@ -25,7 +26,7 @@ import { AuthKeyError } from '../../error' import { bytesFromHex, bytesToHex } from '../../bin' import type { TLFabric } from '../../tl' -import type { TLSchema } from '../../tl/types' +import type { TLSchema } from '../../tl/index.h' import { switchErrors } from './error-cases' import { delayedCall } from '../../util/smart-timeout' @@ -64,6 +65,7 @@ export class ApiManager { mtSchema: TLSchema keyManager: Args networkFabric: any + updatesManager: any auth: any on: On emit: Emit @@ -97,8 +99,12 @@ export class ApiManager { const apiManager = this.mtpInvokeApi apiManager.setUserAuth = this.setUserAuth apiManager.on = this.on + apiManager.emit = this.emit apiManager.storage = storage + // this.updatesManager = UpdatesManager(apiManager) + // apiManager.updates = this.updatesManager + return apiManager } networkSetter = (dc: number, options: LeftOptions) => @@ -167,15 +173,13 @@ export class ApiManager { const networker = await this.mtpGetNetworker(baseDc, opts) const nearestDc = await networker.wrapApiCall( 'help.getNearestDc', {}, opts) - const { nearest_dc } = nearestDc + const { nearest_dc, this_dc } = nearestDc await this.storage.set('dc', nearest_dc) debug(`nearest Dc`)('%O', nearestDc) + if (nearest_dc !== this_dc) await this.mtpGetNetworker(nearest_dc, { createNetworker: true }) } } mtpInvokeApi = async (method: string, params: Object, options: LeftOptions = {}) => { - // const self = this - const defError = new Error() - const stack = defError.stack || 'empty stack' const deferred = blueDefer() const rejectPromise = (error: any) => { let err @@ -188,14 +192,14 @@ export class ApiManager { if (!options.noErrorBox) { //TODO weird code. `error` changed after `.reject`? - //$FlowIssue - err.input = method - //$FlowIssue + + /*err.input = method + err.stack = stack || hasPath(['originalError', 'stack'], error) || error.stack || - (new Error()).stack + (new Error()).stack*/ this.emit('error.invoke', error) } } @@ -279,4 +283,4 @@ const isAnyNetworker = (ctx: ApiManager) => Object.keys(ctx.cache.downloader).le const netError = error => { console.log('Get networker error', error, error.stack) return Promise.reject(error) -} \ No newline at end of file +} diff --git a/src/service/authorizer/index.js b/src/service/authorizer/index.js index 8c01f87..f5fc461 100644 --- a/src/service/authorizer/index.js +++ b/src/service/authorizer/index.js @@ -3,7 +3,7 @@ import Promise from 'bluebird' import blueDefer from '../../util/defer' -import { smartTimeout } from '../../util/smart-timeout' +import { immediate } from '../../util/smart-timeout' import CryptoWorker from '../../crypto' import random from '../secure-random' @@ -15,6 +15,10 @@ import { bytesCmp, bytesToHex, sha1BytesSync, nextRandomInt, import { bpe, str2bigInt, one, dup, sub_, sub, greater } from '../../vendor/leemon' +import Logger from '../../util/log' + +const log = Logger`auth` + // import { ErrorBadResponse } from '../../error' import SendPlainReq from './send-plain-req' @@ -29,12 +33,6 @@ const primeHex = 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d93 'a3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851' + 'f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b' -const asyncLog = (...data) => { - const time = dTime() - console.log(time, ...data) - // setTimeout(() => console.log(time, ...data), 300) -} - const concat = (e1, e2) => [...e1, ...e2] const tmpAesKey = (serverNonce, newNonce) => { @@ -97,75 +95,94 @@ type AuthBasic = { serverSalt: Bytes } +const minSize = Math.ceil(64 / bpe) + 1 + +const getTwoPow = () => { //Dirty hack to count 2^(2048 - 64) + //This number contains 496 zeroes in hex + const arr = Array(496) + .fill('0') + arr.unshift('1') + const hex = arr.join('') + const res = str2bigInt(hex, 16, minSize) + return res +} + +const leemonTwoPow = getTwoPow() + export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, prepare }: Args) => { const sendPlainReq = SendPlainReq({ Serialization, Deserialization }) - function mtpSendReqPQ(auth: AuthBasic) { + async function mtpSendReqPQ(auth: AuthBasic) { const deferred = auth.deferred - asyncLog('Send req_pq', bytesToHex(auth.nonce)) + log('Send req_pq')(bytesToHex(auth.nonce)) const request = Serialization({ mtproto: true }) - + const reqBox = request.writer request.storeMethod('req_pq', { nonce: auth.nonce }) - const keyFoundCheck = key => key - ? auth.publicKey = key - : Promise.reject(new Error('[MT] No public key found')) - const factorizeThunk = () => { - asyncLog('PQ factorization start', auth.pq) - return CryptoWorker.factorize(auth.pq) - } - const factDone = ([ p, q, it ]) => { - auth.p = p - auth.q = q - asyncLog('PQ factorization done', it) - return mtpSendReqDhParams(auth) + let deserializer + try { + await prepare() + deserializer = await sendPlainReq(auth.dcUrl, reqBox.getBuffer()) + } catch (err) { + console.error(dTime(), 'req_pq error', err.message) + deferred.reject(err) + throw err } - const factFail = error => { - asyncLog('Worker error', error, error.stack) - deferred.reject(error) - } - - const factorizer = (deserializer) => { - const response = deserializer.fetchObject('ResPQ') - - if (response._ !== 'resPQ') - throw new Error(`[MT] resPQ response invalid: ${ response._}`) - - if (!bytesCmp(auth.nonce, response.nonce)) - throw new Error('[MT] resPQ nonce mismatch') + try { + const response = deserializer.fetchObject('ResPQ', 'ResPQ') + if (response._ !== 'resPQ') { + const error = new Error(`[MT] resPQ response invalid: ${ response._}`) + deferred.reject(error) + return Promise.reject(error) + } + if (!bytesCmp(auth.nonce, response.nonce)) { + const error = new Error('[MT] resPQ nonce mismatch') + deferred.reject(error) + return Promise.reject(error) + } auth.serverNonce = response.server_nonce auth.pq = response.pq auth.fingerprints = response.server_public_key_fingerprints - asyncLog('Got ResPQ', bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints) + log('Got ResPQ')(bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints) + + const key = await select(auth.fingerprints) + + if (key) + auth.publicKey = key + else { + const error = new Error('[MT] No public key found') + deferred.reject(error) + return Promise.reject(error) + } + log('PQ factorization start')(auth.pq) + const [ p, q, it ] = await CryptoWorker.factorize(auth.pq) - return select(auth.fingerprints) - .then(keyFoundCheck) - .then(factorizeThunk) - .then(factDone, factFail) + auth.p = p + auth.q = q + log('PQ factorization done')(it) + } catch (error) { + log('Worker error')(error, error.stack) + deferred.reject(error) + throw error } - const sendPlainThunk = () => sendPlainReq(auth.dcUrl, request.getBuffer()) - return prepare() - .then(sendPlainThunk) - .then(factorizer, error => { - console.error(dTime(), 'req_pq error', error.message) - deferred.reject(error) - }) + return auth } - function mtpSendReqDhParams(auth: AuthBasic) { + async function mtpSendReqDhParams(auth: AuthBasic) { const deferred = auth.deferred auth.newNonce = new Array(32) random(auth.newNonce) const data = Serialization({ mtproto: true }) + const dataBox = data.writer data.storeObject({ _ : 'p_q_inner_data', pq : auth.pq, @@ -176,9 +193,10 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre new_nonce : auth.newNonce }, 'P_Q_inner_data', 'DECRYPTED_DATA') - const dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes()) + const dataWithHash = sha1BytesSync(dataBox.getBuffer()).concat(data.getBytes()) const request = Serialization({ mtproto: true }) + const reqBox = request.writer request.storeMethod('req_DH_params', { nonce : auth.nonce, server_nonce : auth.serverNonce, @@ -189,47 +207,57 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre }) - const afterReqDH = (deserializer) => { - const response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE') + log('afterReqDH')('Send req_DH_params') - if (response._ !== 'server_DH_params_fail' && response._ !== 'server_DH_params_ok') { - deferred.reject(new Error(`[MT] Server_DH_Params response invalid: ${ response._}`)) - return false - } + let deserializer + try { + deserializer = await sendPlainReq(auth.dcUrl, reqBox.getBuffer()) + } catch (error) { + deferred.reject(error) + throw error + } - if (!bytesCmp(auth.nonce, response.nonce)) { - deferred.reject(new Error('[MT] Server_DH_Params nonce mismatch')) - return false - } - if (!bytesCmp(auth.serverNonce, response.server_nonce)) { - deferred.reject(new Error('[MT] Server_DH_Params server_nonce mismatch')) - return false - } + const response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE') - if (response._ === 'server_DH_params_fail') { - const newNonceHash = sha1BytesSync(auth.newNonce).slice(-16) - if (!bytesCmp(newNonceHash, response.new_nonce_hash)) { - deferred.reject(new Error('[MT] server_DH_params_fail new_nonce_hash mismatch')) - return false - } - deferred.reject(new Error('[MT] server_DH_params_fail')) - return false - } + if (response._ !== 'server_DH_params_fail' && response._ !== 'server_DH_params_ok') { + const error = new Error(`[MT] Server_DH_Params response invalid: ${ response._}`) + deferred.reject(error) + throw error + } - // try { - mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer) - // } catch (e) { - // deferred.reject(e) - // return false - // } + if (!bytesCmp(auth.nonce, response.nonce)) { + const error = new Error('[MT] Server_DH_Params nonce mismatch') + deferred.reject(error) + throw error + } - mtpSendSetClientDhParams(auth) + if (!bytesCmp(auth.serverNonce, response.server_nonce)) { + const error = new Error('[MT] Server_DH_Params server_nonce mismatch') + deferred.reject(error) + throw error } - asyncLog('Send req_DH_params') - return sendPlainReq(auth.dcUrl, request.getBuffer()) - .then(afterReqDH, deferred.reject) + if (response._ === 'server_DH_params_fail') { + const newNonceHash = sha1BytesSync(auth.newNonce).slice(-16) + if (!bytesCmp(newNonceHash, response.new_nonce_hash)) { + const error = new Error('[MT] server_DH_params_fail new_nonce_hash mismatch') + deferred.reject(error) + throw error + } + const error = new Error('[MT] server_DH_params_fail') + deferred.reject(error) + throw error + } + + // try { + mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer) + // } catch (e) { + // deferred.reject(e) + // return false + // } + + return auth } function mtpDecryptServerDhDataAnswer(auth: AuthBasic, encryptedAnswer) { @@ -246,7 +274,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre const buffer = bytesToArrayBuffer(answerWithPadding) const deserializer = Deserialization(buffer, { mtproto: true }) - const response = deserializer.fetchObject('Server_DH_inner_data') + const response = deserializer.fetchObject('Server_DH_inner_data', 'server_dh') if (response._ !== 'server_DH_inner_data') throw new Error(`[MT] server_DH_inner_data response invalid`) @@ -257,7 +285,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre if (!bytesCmp(auth.serverNonce, response.server_nonce)) throw new Error('[MT] server_DH_inner_data serverNonce mismatch') - asyncLog('Done decrypting answer') + log('DecryptServerDhDataAnswer')('Done decrypting answer') auth.g = response.g auth.dhPrime = response.dh_prime auth.gA = response.g_a @@ -275,27 +303,14 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre applyServerTime(auth.serverTime, auth.localTime) } - const minSize = Math.ceil(64 / bpe) + 1 - - const getTwoPow = () => { //Dirty hack to count 2^(2048 - 64) - //This number contains 496 zeroes in hex - const arr = Array(496) - .fill('0') - arr.unshift('1') - const hex = arr.join('') - const res = str2bigInt(hex, 16, minSize) - return res - } - - const leemonTwoPow = getTwoPow() - function mtpVerifyDhParams(g, dhPrime, gA) { - asyncLog('Verifying DH params') + const innerLog = log('VerifyDhParams') + innerLog('begin') const dhPrimeHex = bytesToHex(dhPrime) if (g !== 3 || dhPrimeHex !== primeHex) // The verified value is from https://core.telegram.org/mtproto/security_guidelines throw new Error('[MT] DH params are not verified: unknown dhPrime') - asyncLog('dhPrime cmp OK') + innerLog('dhPrime cmp OK') // const gABigInt = new BigInteger(bytesToHex(gA), 16) // const dhPrimeBigInt = new BigInteger(dhPrimeHex, 16) @@ -334,123 +349,122 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}') if (case4) throw new Error('[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}') - asyncLog('2^{2048-64} < gA < dhPrime-2^{2048-64} OK') + innerLog('2^{2048-64} < gA < dhPrime-2^{2048-64} OK') return true } - function mtpSendSetClientDhParams(auth: AuthBasic) { + async function mtpSendSetClientDhParams(auth: AuthBasic) { const deferred = auth.deferred const gBytes = bytesFromHex(auth.g.toString(16)) auth.b = new Array(256) random(auth.b) + const gB = await CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime) + const data = Serialization({ mtproto: true }) + + data.storeObject({ + _ : 'client_DH_inner_data', + nonce : auth.nonce, + server_nonce: auth.serverNonce, + retry_id : [0, auth.retry++], + g_b : gB + }, 'Client_DH_Inner_Data', 'client_DH') + const dataWithHash = sha1BytesSync(data.writer.getBuffer()).concat(data.getBytes()) - const afterPlainRequest = (deserializer) => { - const response = deserializer.fetchObject('Set_client_DH_params_answer') - - const onAnswer = (authKey) => { - const authKeyHash = sha1BytesSync(authKey), - authKeyAux = authKeyHash.slice(0, 8), - authKeyID = authKeyHash.slice(-8) - - asyncLog('Got Set_client_DH_params_answer', response._) - switch (response._) { - case 'dh_gen_ok': { - const newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16) - - if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch')) - return false - } - - const serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8)) - // console.log('Auth successfull!', authKeyID, authKey, serverSalt) - - auth.authKeyID = authKeyID - auth.authKey = authKey - auth.serverSalt = serverSalt - - deferred.resolve(auth) - break - } - case 'dh_gen_retry': { - const newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16) - if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch')) - return false - } - - return mtpSendSetClientDhParams(auth) - } - case 'dh_gen_fail': { - const newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16) - if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch')) - return false - } - - deferred.reject(new Error('[MT] Set_client_DH_params_answer fail')) - return false - } + const encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv) + + const request = Serialization({ mtproto: true }) + + request.storeMethod('set_client_DH_params', { + nonce : auth.nonce, + server_nonce : auth.serverNonce, + encrypted_data: encryptedData + }) + + log('onGb')('Send set_client_DH_params') + + const deserializer = await sendPlainReq(auth.dcUrl, request.writer.getBuffer()) + + const response = deserializer.fetchObject('Set_client_DH_params_answer', 'client_dh') + + if (response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') { + const error = new Error(`[MT] Set_client_DH_params_answer response invalid: ${ response._}`) + deferred.reject(error) + throw error + } + + if (!bytesCmp(auth.nonce, response.nonce)) { + const error = new Error('[MT] Set_client_DH_params_answer nonce mismatch') + deferred.reject(error) + throw error + } + + if (!bytesCmp(auth.serverNonce, response.server_nonce)) { + const error = new Error('[MT] Set_client_DH_params_answer server_nonce mismatch') + deferred.reject(error) + throw error + } + + const authKey = await CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime) + + const authKeyHash = sha1BytesSync(authKey), + authKeyAux = authKeyHash.slice(0, 8), + authKeyID = authKeyHash.slice(-8) + + log('Got Set_client_DH_params_answer')(response._) + switch (response._) { + case 'dh_gen_ok': { + const newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16) + + if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) { + deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch')) + return false } - } - if (response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') { - deferred.reject(new Error(`[MT] Set_client_DH_params_answer response invalid: ${ response._}`)) - return false + const serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8)) + // console.log('Auth successfull!', authKeyID, authKey, serverSalt) + + auth.authKeyID = authKeyID + auth.authKey = authKey + auth.serverSalt = serverSalt + + deferred.resolve(auth) + break } + case 'dh_gen_retry': { + const newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16) + if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) { + deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch')) + return false + } - if (!bytesCmp(auth.nonce, response.nonce)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer nonce mismatch')) - return false + return mtpSendSetClientDhParams(auth) } + case 'dh_gen_fail': { + const newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16) + if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) { + deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch')) + return false + } - if (!bytesCmp(auth.serverNonce, response.server_nonce)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer server_nonce mismatch')) + deferred.reject(new Error('[MT] Set_client_DH_params_answer fail')) return false } - - return CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime) - .then(onAnswer) - } - - const onGb = (gB) => { - const data = Serialization({ mtproto: true }) - data.storeObject({ - _ : 'client_DH_inner_data', - nonce : auth.nonce, - server_nonce: auth.serverNonce, - retry_id : [0, auth.retry++], - g_b : gB - }, 'Client_DH_Inner_Data') - - const dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes()) - - const encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv) - - const request = Serialization({ mtproto: true }) - request.storeMethod('set_client_DH_params', { - nonce : auth.nonce, - server_nonce : auth.serverNonce, - encrypted_data: encryptedData - }) - - asyncLog('Send set_client_DH_params') - return sendPlainReq(auth.dcUrl, request.getBuffer()) - .then(afterPlainRequest) } - - return CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime) - .then(onGb) } + const authChain = (auth: AuthBasic) => + mtpSendReqPQ(auth) + .then(mtpSendReqDhParams) + .then(mtpSendSetClientDhParams) + function mtpAuth(dcID: number, cached: Cached, dcUrl: string) { if (cached[dcID]) return cached[dcID].promise - asyncLog('mtpAuth') + log('mtpAuth', 'dcID', 'dcUrl')(dcID, dcUrl) const nonce = [] for (let i = 0; i < 16; i++) nonce.push(nextRandomInt(0xFF)) @@ -466,7 +480,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre deferred: blueDefer() } - smartTimeout.immediate(() => mtpSendReqPQ(auth)) + immediate(authChain, auth) cached[dcID] = auth.deferred diff --git a/src/service/authorizer/send-plain-req.js b/src/service/authorizer/send-plain-req.js index 65ab68d..963e11d 100644 --- a/src/service/authorizer/send-plain-req.js +++ b/src/service/authorizer/send-plain-req.js @@ -9,6 +9,7 @@ import allPass from 'ramda/src/allPass' import httpClient from '../../http' import { ErrorBadResponse, ErrorNotFound } from '../../error' import { generateID } from '../time-manager' +import { WriteMediator, ReadMediator } from '../../tl' import type { TLFabric } from '../../tl' @@ -21,11 +22,13 @@ const SendPlain = ({ Serialization, Deserialization }: TLFabric) => { requestArray = new Int32Array(requestBuffer) const header = Serialization() - header.storeLongP(0, 0, 'auth_key_id') // Auth key - header.storeLong(generateID(), 'msg_id') // Msg_id - header.storeInt(requestLength, 'request_length') + const headBox = header.writer - const headerBuffer: ArrayBuffer = header.getBuffer(), + WriteMediator.longP(headBox, 0, 0, 'auth_key_id') // Auth key + WriteMediator.long(headBox, generateID(), 'msg_id') // Msg_id + WriteMediator.int(headBox, requestLength, 'request_length') + + const headerBuffer: ArrayBuffer = headBox.getBuffer(), headerArray = new Int32Array(headerBuffer) const headerLength = headerBuffer.byteLength @@ -68,9 +71,10 @@ const SendPlain = ({ Serialization, Deserialization }: TLFabric) => { let deserializer try { deserializer = Deserialization(req.data, { mtproto: true }) - deserializer.fetchLong('auth_key_id') - deserializer.fetchLong('msg_id') - deserializer.fetchInt('msg_len') + const ctx = deserializer.typeBuffer + ReadMediator.long(ctx, 'auth_key_id') + ReadMediator.long(ctx, 'msg_id') + ReadMediator.int(ctx, 'msg_len') } catch (e) { return Promise.reject(new ErrorBadResponse(url, e)) } diff --git a/src/service/main/index.h.js b/src/service/main/index.h.js index 66299fd..d020d8b 100644 --- a/src/service/main/index.h.js +++ b/src/service/main/index.h.js @@ -1,6 +1,6 @@ //@flow -import type { TLSchema } from '../../tl/types' +import type { TLSchema } from '../../tl/index.h' export type ApiConfig = { invokeWithLayer?: number, diff --git a/src/service/main/index.js b/src/service/main/index.js index 809b05c..0b485a3 100644 --- a/src/service/main/index.js +++ b/src/service/main/index.js @@ -8,6 +8,7 @@ import { PureStorage } from '../../store' import TL from '../../tl' import configValidator from './config-validation' +import generateInvokeLayer from './invoke-layer-generator' import type { TLFabric } from '../../tl' import type { ApiConfig, ConfigType, StrictConfig, Emit, On, PublicKey } from './index.h' @@ -58,6 +59,8 @@ const configNormalization = (config: ConfigType): StrictConfig => { mtSchema = mtproto57, } = config const apiNormalized = { ...apiConfig, ...api } + const invokeLayer = generateInvokeLayer(apiNormalized.layer) + apiNormalized.invokeWithLayer = invokeLayer const fullCfg = { server, api: apiNormalized, diff --git a/src/service/main/invoke-layer-generator.js b/src/service/main/invoke-layer-generator.js new file mode 100644 index 0000000..7296135 --- /dev/null +++ b/src/service/main/invoke-layer-generator.js @@ -0,0 +1,36 @@ +//@flow + +/** + * Defines the parameter required for authorization based on the level number + * + * Values were taken from here + * https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/scheme.tl + * + * @param {number} apiLevel + * @returns + */ +function generateInvokeLayer(apiLevel: number) { + switch (apiLevel) { + case 1: return 0x53835315 + case 2: return 0x289dd1f6 + case 3: return 0xb7475268 + case 4: return 0xdea0d430 + case 5: return 0x417a57ae + case 6: return 0x3a64d54d + case 7: return 0xa5be56d3 + case 8: return 0xe9abd9fd + case 9: return 0x76715a63 + case 10: return 0x39620c41 + case 11: return 0xa6b88fdf + case 12: return 0xdda60d3c + case 13: return 0x427c8ea2 + case 14: return 0x2b9b08fa + case 15: return 0xb4418b64 + case 16: return 0xcf5f0987 + case 17: return 0x50858a19 + case 18: return 0x1c900537 + default: return 0xda9b0d0d + } +} + +export default generateInvokeLayer \ No newline at end of file diff --git a/src/service/networker/index.js b/src/service/networker/index.js index 063aa31..cd66044 100644 --- a/src/service/networker/index.js +++ b/src/service/networker/index.js @@ -26,6 +26,7 @@ import { convertToUint8Array, convertToArrayBuffer, sha1BytesSync, bytesToArrayBuffer, longToBytes, uintToInt, rshift32 } from '../../bin' import type { TLFabric, SerializationFabric, DeserializationFabric } from '../../tl' +import { WriteMediator, ReadMediator, TypeWriter } from '../../tl' import type { Emit } from '../main/index.h' let updatesProcessor @@ -50,6 +51,14 @@ type ContextConfig = { emit: Emit } +const storeIntString = (writer: TypeWriter) => (value: number | string, field: string) => { + switch (typeof value) { + case 'string': return WriteMediator.bytes(writer, value, `${field}:string`) + case 'number': return WriteMediator.int(writer, value, field) + default: throw new Error(`tl storeIntString field ${field} value type ${typeof value}`) + } +} + export class NetworkerThread { dcID: number authKey: string @@ -187,7 +196,7 @@ export class NetworkerThread { wrapApiCall(method: string, params: Object, options: NetOptions) { const serializer = this.Serialization(options) - + const serialBox = serializer.writer if (!this.connectionInited) { // serializer.storeInt(0xda9b0d0d, 'invokeWithLayer') // serializer.storeInt(Config.Schema.API.layer, 'layer') @@ -197,12 +206,13 @@ export class NetworkerThread { // serializer.storeString(navigator.platform || 'Unknown Platform', 'system_version') // serializer.storeString(Config.App.version, 'app_version') // serializer.storeString(navigator.language || 'en', 'lang_code') - mapObjIndexed(serializer.storeIntString, this.appConfig) + const mapper = storeIntString(serialBox) + mapObjIndexed(mapper, this.appConfig) } if (options.afterMessageID) { - serializer.storeInt(0xcb9f372d, 'invokeAfterMsg') - serializer.storeLong(options.afterMessageID, 'msg_id') + WriteMediator.int(serialBox, 0xcb9f372d, 'invokeAfterMsg') + WriteMediator.long(serialBox, options.afterMessageID, 'msg_id') } options.resultType = serializer.storeMethod(method, params) @@ -499,16 +509,17 @@ export class NetworkerThread { if (messages.length > 1) { const container = this.Serialization({ mtproto: true, startMaxLength: messagesByteLen + 64 }) - container.storeInt(0x73f1f8dc, 'CONTAINER[id]') - container.storeInt(messages.length, 'CONTAINER[count]') + const contBox = container.writer + WriteMediator.int(contBox, 0x73f1f8dc, 'CONTAINER[id]') + WriteMediator.int(contBox, messages.length, 'CONTAINER[count]') const innerMessages = [] let i = 0 for (const msg of messages) { - container.storeLong(msg.msg_id, `CONTAINER[${i}][msg_id]`) + WriteMediator.long(contBox, msg.msg_id, `CONTAINER[${i}][msg_id]`) innerMessages.push(msg.msg_id) - container.storeInt(msg.seq_no, `CONTAINER[${i}][seq_no]`) - container.storeInt(msg.body.length, `CONTAINER[${i}][bytes]`) - container.storeRawBytes(msg.body, `CONTAINER[${i}][body]`) + WriteMediator.int(contBox, msg.seq_no, `CONTAINER[${i}][seq_no]`) + WriteMediator.int(contBox, msg.body.length, `CONTAINER[${i}][bytes]`) + WriteMediator.intBytes(contBox, msg.body, false, `CONTAINER[${i}][body]`) if (msg.noResponse) noResponseMsgs.push(msg.msg_id) i++ @@ -580,19 +591,18 @@ export class NetworkerThread { // console.log(dTime(), 'Send encrypted'/*, message*/) // console.trace() const data = this.Serialization({ startMaxLength: message.body.length + 64 }) + const dataBox = data.writer + WriteMediator.intBytes(dataBox, this.serverSalt, 64, 'salt') + WriteMediator.intBytes(dataBox, this.sessionID, 64, 'session_id') + WriteMediator.long(dataBox, message.msg_id, 'message_id') + WriteMediator.int(dataBox, message.seq_no, 'seq_no') - data.storeIntBytes(this.serverSalt, 64, 'salt') - data.storeIntBytes(this.sessionID, 64, 'session_id') - - data.storeLong(message.msg_id, 'message_id') - data.storeInt(message.seq_no, 'seq_no') - - data.storeInt(message.body.length, 'message_data_length') - data.storeRawBytes(message.body, 'message_data') + WriteMediator.int(dataBox, message.body.length, 'message_data_length') + WriteMediator.intBytes(dataBox, message.body, false, 'message_data') const url = this.chooseServer(this.dcID, this.upload) - const bytes = data.getBuffer() + const bytes = dataBox.getBuffer() const bytesHash = await CryptoWorker.sha1Hash(bytes) const msgKey = new Uint8Array(bytesHash).subarray(4, 20) @@ -600,13 +610,14 @@ export class NetworkerThread { const encryptedBytes = await CryptoWorker.aesEncrypt(bytes, keyIv[0], keyIv[1]) const request = this.Serialization({ startMaxLength: encryptedBytes.byteLength + 256 }) - request.storeIntBytes(this.authKeyID, 64, 'auth_key_id') - request.storeIntBytes(msgKey, 128, 'msg_key') - request.storeRawBytes(encryptedBytes, 'encrypted_data') + const requestBox = request.writer + WriteMediator.intBytes(requestBox, this.authKeyID, 64, 'auth_key_id') + WriteMediator.intBytes(requestBox, msgKey, 128, 'msg_key') + WriteMediator.intBytes(requestBox, encryptedBytes, false, 'encrypted_data') const requestData = xhrSendBuffer - ? request.getBuffer() - : request.getArray() + ? requestBox.getArray().buffer + : requestBox.getArray() options = { responseType: 'arraybuffer', ...options } @@ -622,7 +633,7 @@ export class NetworkerThread { getMsgById = ({ req_msg_id }) => this.state.getSent(req_msg_id) - async parseResponse(responseBuffer) { + async parseResponse(responseBuffer: Uint8Array) { // console.log(dTime(), 'Start parsing response') // const self = this @@ -645,7 +656,7 @@ export class NetworkerThread { deserializer.fetchIntBytes(64, 'salt') const sessionID = deserializer.fetchIntBytes(64, 'session_id') - const messageID = deserializer.fetchLong('message_id') + const messageID = ReadMediator.long( deserializer.typeBuffer, 'message_id') const isInvalidSession = !bytesCmp(sessionID, this.sessionID) && ( @@ -968,10 +979,9 @@ const getDeserializeOpts = msgGetter => ({ mtproto : true, override: { mt_message(result, field) { - result.msg_id = this.fetchLong(`${ field }[msg_id]`) - result.seqno = this.fetchInt(`${ field }[seqno]`) - //TODO WARN! Why everywhere seqno is seq_no and only there its seqno?!? - result.bytes = this.fetchInt(`${ field }[bytes]`) + result.msg_id = ReadMediator.long( this.typeBuffer, `${ field }[msg_id]`) + result.seqno = ReadMediator.int( this.typeBuffer, `${ field }[seqno]`) + result.bytes = ReadMediator.int( this.typeBuffer, `${ field }[bytes]`) const offset = this.getOffset() @@ -981,15 +991,15 @@ const getDeserializeOpts = msgGetter => ({ console.error(dTime(), 'parse error', e.message, e.stack) result.body = { _: 'parse_error', error: e } } - if (this.offset != offset + result.bytes) { + if (this.typeBuffer.offset != offset + result.bytes) { // console.warn(dTime(), 'set offset', this.offset, offset, result.bytes) // console.log(dTime(), result) - this.offset = offset + result.bytes + this.typeBuffer.offset = offset + result.bytes } // console.log(dTime(), 'override message', result) }, - mt_rpc_result(result, field) { - result.req_msg_id = this.fetchLong(`${ field }[req_msg_id]`) + mt_rpc_result(result, field: string) { + result.req_msg_id = ReadMediator.long( this.typeBuffer, `${ field }[req_msg_id]`) const sentMessage = msgGetter(result) const type = sentMessage && sentMessage.resultType || 'Object' diff --git a/src/service/rsa-keys-manger.js b/src/service/rsa-keys-manger.js index 324c1fc..0ca851d 100644 --- a/src/service/rsa-keys-manger.js +++ b/src/service/rsa-keys-manger.js @@ -1,18 +1,29 @@ +//@flow + import Promise from 'bluebird' + +import type { PublicKey } from './main/index.h' +import type { Cached } from './api-manager/index.h' +import type { SerializationFabric } from '../tl' + +import { WriteMediator } from '../tl' + import { bytesToHex, sha1BytesSync, bytesFromHex, strDecToHex } from '../bin' -import type { SerializationFabric } from '../tl' -export const KeyManager = (Serialization: SerializationFabric, publisKeysHex, publicKeysParsed) => { +export const KeyManager = (Serialization: SerializationFabric, + publisKeysHex: PublicKey[], + publicKeysParsed: Cached) => { let prepared = false - const mapPrepare = ({ modulus, exponent }) => { + const mapPrepare = ({ modulus, exponent }: PublicKey) => { const RSAPublicKey = Serialization() - RSAPublicKey.storeBytes(bytesFromHex(modulus), 'n') - RSAPublicKey.storeBytes(bytesFromHex(exponent), 'e') + const rsaBox = RSAPublicKey.writer + WriteMediator.bytes(rsaBox, bytesFromHex(modulus), 'n') + WriteMediator.bytes(rsaBox, bytesFromHex(exponent), 'e') - const buffer = RSAPublicKey.getBuffer() + const buffer = rsaBox.getBuffer() const fingerprintBytes = sha1BytesSync(buffer).slice(-8) fingerprintBytes.reverse() @@ -31,15 +42,15 @@ export const KeyManager = (Serialization: SerializationFabric, publisKeysHex, pu prepared = true } - async function selectRsaKeyByFingerPrint(fingerprints) { + async function selectRsaKeyByFingerPrint(fingerprints: string[]) { await prepareRsaKeys() let fingerprintHex, foundKey - for (let i = 0; i < fingerprints.length; i++) { - fingerprintHex = strDecToHex(fingerprints[i]) + for (const fingerprint of fingerprints) { + fingerprintHex = strDecToHex(fingerprint) foundKey = publicKeysParsed[fingerprintHex] if (foundKey) - return { fingerprint: fingerprints[i], ...foundKey } + return { fingerprint, ...foundKey } } return false } diff --git a/src/service/time-manager.js b/src/service/time-manager.js index 77f9b53..7435f1e 100644 --- a/src/service/time-manager.js +++ b/src/service/time-manager.js @@ -3,6 +3,10 @@ import isNode from 'detect-node' import { TimeOffset } from '../store' import { nextRandomInt, lshift32 } from '../bin' +import Logger from '../util/log' + +const log = Logger`time-manager` + export const tsNow = seconds => { let t = +new Date() //eslint-disable-next-line @@ -12,9 +16,7 @@ export const tsNow = seconds => { : t } -const logTimer = (new Date()).getTime() - -export const dTime = () => `[${(((new Date()).getTime() -logTimer) / 1000).toFixed(3)}]` +export { dTime } from '../util/dtime' let lastMessageID = [0, 0] let timerOffset = 0 @@ -48,7 +50,7 @@ export const applyServerTime = (serverTime, localTime) => { lastMessageID = [0, 0] timerOffset = newTimeOffset - console.log(dTime(), 'Apply server time', serverTime, localTime, newTimeOffset, changed) + log('Apply server time')(serverTime, localTime, newTimeOffset, changed) return changed } diff --git a/src/service/updates.h.js b/src/service/updates.h.js new file mode 100644 index 0000000..c744be5 --- /dev/null +++ b/src/service/updates.h.js @@ -0,0 +1,10 @@ +export type CurState = { + [k: string]: any; + syncPending?: { + ptsAwaiting?: ?boolean; + seqAwaiting?: ?boolean; + } +} + +export type UpdatesState = any; + diff --git a/src/service/updates.js b/src/service/updates.js new file mode 100644 index 0000000..7a955fa --- /dev/null +++ b/src/service/updates.js @@ -0,0 +1,452 @@ +//@flow + +import Promise from 'bluebird' +import Logger from '../util/log' +const debug = Logger`updates` + +import { setUpdatesProcessor } from './networker' +import type { ApiManagerInstance } from './api-manager/index.h' +import type { UpdatesState, CurState } from './updates.h' + +// const AppPeersManager = null +// const AppUsersManager = null +const AppChatsManager = null + +const UpdatesManager = (api: ApiManagerInstance) => { + const updatesState: any = { + pendingPtsUpdates: [], + pendingSeqUpdates: {}, + syncPending : false, + syncLoading : true + } + const channelStates = {} + + let myID = 0 + getUserID().then(id => myID = id) + + async function getUserID() { + const auth = await api.storage.get('user_auth') + return auth.id || 0 + } + + function popPendingSeqUpdate() { + const nextSeq = updatesState.seq + 1 + const pendingUpdatesData = updatesState.pendingSeqUpdates[nextSeq] + if (!pendingUpdatesData) { + return false + } + const updates = pendingUpdatesData.updates + updates.forEach(saveUpdate) + updatesState.seq = pendingUpdatesData.seq + if (pendingUpdatesData.date && updatesState.date < pendingUpdatesData.date) { + updatesState.date = pendingUpdatesData.date + } + delete updatesState.pendingSeqUpdates[nextSeq] + + if (!popPendingSeqUpdate() && + updatesState.syncPending && + updatesState.syncPending.seqAwaiting && + updatesState.seq >= updatesState.syncPending.seqAwaiting) { + if (!updatesState.syncPending.ptsAwaiting) { + clearTimeout(updatesState.syncPending.timeout) + updatesState.syncPending = false + } else { + delete updatesState.syncPending.seqAwaiting + } + } + + return true + } + + function popPendingPtsUpdate(channelID) { + const curState = channelID ? getChannelState(channelID) : updatesState + if (!curState.pendingPtsUpdates.length) { + return false + } + curState.pendingPtsUpdates.sort((a, b) => a.pts - b.pts) + + let curPts = curState.pts + let goodPts = false + let goodIndex = 0 + let update + let i = 0 + for (const update of curState.pendingPtsUpdates) { + curPts += update.pts_count + if (curPts >= update.pts) { + goodPts = update.pts + goodIndex = i + } + i++ + } + + if (!goodPts) { + return false + } + + debug('pop pending pts updates')(goodPts, curState.pendingPtsUpdates.slice(0, goodIndex + 1)) + + curState.pts = goodPts + for (let i = 0; i <= goodIndex; i++) { + update = curState.pendingPtsUpdates[i] + saveUpdate(update) + } + curState.pendingPtsUpdates.splice(0, goodIndex + 1) + + if (!curState.pendingPtsUpdates.length && curState.syncPending) { + if (!curState.syncPending.seqAwaiting) { + clearTimeout(curState.syncPending.timeout) + curState.syncPending = false + } else { + delete curState.syncPending.ptsAwaiting + } + } + + return true + } + + function forceGetDifference() { + if (!updatesState.syncLoading) { + getDifference() + } + } + + function processUpdateMessage(updateMessage: any) { + // return forceGetDifference() + const processOpts = { + date : updateMessage.date, + seq : updateMessage.seq, + seqStart: updateMessage.seq_start + } + + switch (updateMessage._) { + case 'updatesTooLong': + case 'new_session_created': + forceGetDifference() + break + + case 'updateShort': + processUpdate(updateMessage.update, processOpts) + break + + case 'updateShortMessage': + case 'updateShortChatMessage': { + const isOut = updateMessage.flags & 2 + const fromID = updateMessage.from_id || (isOut ? myID : updateMessage.user_id) + const toID = updateMessage.chat_id + ? -updateMessage.chat_id + : isOut ? updateMessage.user_id : myID + + api.emit('updateShortMessage', { + processUpdate, + processOpts, + updateMessage, + fromID, + toID + }) + } + break + + case 'updatesCombined': + case 'updates': + api.emit('apiUpdate', updateMessage) + + updateMessage.updates.forEach(update => { + processUpdate(update, processOpts) + }) + break + + default: + debug('Unknown update message')(updateMessage) + } + } + + async function getDifference() { + if (!updatesState.syncLoading) { + updatesState.syncLoading = true + updatesState.pendingSeqUpdates = {} + updatesState.pendingPtsUpdates = [] + } + + if (updatesState.syncPending) { + clearTimeout(updatesState.syncPending.timeout) + updatesState.syncPending = false + } + + const differenceResult = await api('updates.getDifference', { + pts : updatesState.pts, + date: updatesState.date, + qts : -1 + }) + if (differenceResult._ === 'updates.differenceEmpty') { + debug('apply empty diff')(differenceResult.seq) + updatesState.date = differenceResult.date + updatesState.seq = differenceResult.seq + updatesState.syncLoading = false + api.emit('stateSynchronized') + return false + } + + api.emit('difference', differenceResult) + + // Should be first because of updateMessageID + // console.log(dT(), 'applying', differenceResult.other_updates.length, 'other updates') + + const channelsUpdates = [] + differenceResult.other_updates.forEach(update => { + switch (update._) { + case 'updateChannelTooLong': + case 'updateNewChannelMessage': + case 'updateEditChannelMessage': + processUpdate(update) + return + } + saveUpdate(update) + }) + + // console.log(dT(), 'applying', differenceResult.new_messages.length, 'new messages') + differenceResult.new_messages.forEach(apiMessage => { + saveUpdate({ + _ : 'updateNewMessage', + message : apiMessage, + pts : updatesState.pts, + pts_count: 0 + }) + }) + + const nextState = differenceResult.intermediate_state || differenceResult.state + updatesState.seq = nextState.seq + updatesState.pts = nextState.pts + updatesState.date = nextState.date + + // console.log(dT(), 'apply diff', updatesState.seq, updatesState.pts) + + if (differenceResult._ == 'updates.differenceSlice') { + getDifference() + } else { + // console.log(dT(), 'finished get diff') + api.emit('stateSynchronized') + updatesState.syncLoading = false + } + } + + async function getChannelDifference(channelID: number) { + const channelState = getChannelState(channelID) + if (!channelState.syncLoading) { + channelState.syncLoading = true + channelState.pendingPtsUpdates = [] + } + if (channelState.syncPending) { + clearTimeout(channelState.syncPending.timeout) + channelState.syncPending = false + } + // console.log(dT(), 'Get channel diff', AppChatsManager.getChat(channelID), channelState.pts) + const differenceResult = await api('updates.getChannelDifference', { + channel: AppChatsManager.getChannelInput(channelID), + filter : { _: 'channelMessagesFilterEmpty' }, + pts : channelState.pts, + limit : 30 + }) + // console.log(dT(), 'channel diff result', differenceResult) + channelState.pts = differenceResult.pts + + if (differenceResult._ == 'updates.channelDifferenceEmpty') { + debug('apply channel empty diff')(differenceResult) + channelState.syncLoading = false + api.emit('stateSynchronized') + return false + } + + if (differenceResult._ == 'updates.channelDifferenceTooLong') { + debug('channel diff too long')(differenceResult) + channelState.syncLoading = false + delete channelStates[channelID] + saveUpdate({ _: 'updateChannelReload', channel_id: channelID }) + return false + } + + api.emit('difference', differenceResult) + + // Should be first because of updateMessageID + debug('applying')(differenceResult.other_updates.length, 'channel other updates') + differenceResult.other_updates.map(saveUpdate) + + debug('applying')(differenceResult.new_messages.length, 'channel new messages') + differenceResult.new_messages.forEach(apiMessage => { + saveUpdate({ + _ : 'updateNewChannelMessage', + message : apiMessage, + pts : channelState.pts, + pts_count: 0 + }) + }) + + debug('apply channel diff')(channelState.pts) + + if (differenceResult._ == 'updates.channelDifference' && + !differenceResult.pFlags['final']) { + getChannelDifference(channelID) + } else { + debug('finished channel get diff')() + api.emit('stateSynchronized') + channelState.syncLoading = false + } + } + + function addChannelState(channelID: number, pts: ?number) { + if (!pts) { + throw new Error(`Add channel state without pts ${channelID}`) + } + if (channelStates[channelID] === undefined) { + channelStates[channelID] = { + pts, + pendingPtsUpdates: [], + syncPending : false, + syncLoading : false + } + return true + } + return false + } + + function getChannelState(channelID: number, pts?: ?number) { + if (channelStates[channelID] === undefined) { + addChannelState(channelID, pts) + } + return channelStates[channelID] + } + + function processUpdate(update, options = {}) { + let channelID + switch (update._) { + case 'updateNewChannelMessage': + case 'updateEditChannelMessage': + channelID = update.message.to_id.channel_id || update.message.to_id.chat_id + break + case 'updateDeleteChannelMessages': + channelID = update.channel_id + break + case 'updateChannelTooLong': + channelID = update.channel_id + if (channelStates[channelID] === undefined) { + return false + } + break + } + + const curState: CurState = channelID ? getChannelState(channelID, update.pts) : updatesState + + // console.log(dT(), 'process', channelID, curState.pts, update) + + if (curState.syncLoading) { + return false + } + + if (update._ == 'updateChannelTooLong') { + getChannelDifference(channelID || 0) + return false + } + + let popPts + let popSeq + + if (update.pts) { + const newPts = curState.pts + (update.pts_count || 0) + if (newPts < update.pts) { + // debug('Pts hole')(curState, update, channelID && AppChatsManager.getChat(channelID)) + curState.pendingPtsUpdates.push(update) + if (!curState.syncPending) { + curState.syncPending = { + timeout: setTimeout(() => { + if (channelID) { + getChannelDifference(channelID) + } else { + getDifference() + } + }, 5000), + } + } + curState.syncPending.ptsAwaiting = true + return false + } + if (update.pts > curState.pts) { + curState.pts = update.pts + popPts = true + } + else if (update.pts_count) { + // console.warn(dT(), 'Duplicate update', update) + return false + } + if (channelID && options.date && updatesState.date < options.date) { + updatesState.date = options.date + } + } + else if (!channelID && options.seq > 0) { + const seq = options.seq + const seqStart = options.seqStart || seq + + if (seqStart != curState.seq + 1) { + if (seqStart > curState.seq) { + debug('Seq hole')(curState, curState.syncPending && curState.syncPending.seqAwaiting) + + if (curState.pendingSeqUpdates[seqStart] === undefined) { + curState.pendingSeqUpdates[seqStart] = { seq, date: options.date, updates: [] } + } + curState.pendingSeqUpdates[seqStart].updates.push(update) + + if (!curState.syncPending) { + curState.syncPending = { + timeout: setTimeout(() => { + getDifference() + }, 5000) + } + } + if (!curState.syncPending.seqAwaiting || + curState.syncPending.seqAwaiting < seqStart) { + curState.syncPending.seqAwaiting = seqStart + } + return false + } + } + + if (curState.seq != seq) { + curState.seq = seq + if (options.date && curState.date < options.date) { + curState.date = options.date + } + popSeq = true + } + } + + saveUpdate(update) + + if (popPts) { + popPendingPtsUpdate(channelID) + } + else if (popSeq) { + popPendingSeqUpdate() + } + } + + function saveUpdate(update: any) { + api.emit('apiUpdate', update) + } + + async function attach() { + setUpdatesProcessor(processUpdateMessage) + const stateResult: UpdatesState = await api('updates.getState', {}, { noErrorBox: true }) + updatesState.seq = stateResult.seq + updatesState.pts = stateResult.pts + updatesState.date = stateResult.date + setTimeout(() => { + updatesState.syncLoading = false + }, 1000) + } + + return { + processUpdateMessage, + addChannelState, + attach + } +} + +export default UpdatesManager \ No newline at end of file diff --git a/src/tl/index.h.js b/src/tl/index.h.js new file mode 100644 index 0000000..f26dc45 --- /dev/null +++ b/src/tl/index.h.js @@ -0,0 +1,28 @@ +//@flow + +export type BinaryData = number[] | Uint8Array + +export type TLParam = { + name: string, + type: string +} + +export type TLConstruct = { + id: string, + type: string, + predicate: string, + params: TLParam[] +} + +export type TLMethod = { + id: string, + type: string, + method: string, + params: TLParam[] +} + +export type TLSchema = { + constructors: TLConstruct[], + methods: TLMethod[], + constructorsIndex?: number[] +} \ No newline at end of file diff --git a/src/tl/index.js b/src/tl/index.js index d6e6f45..bfdbdbb 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -1,252 +1,108 @@ //@flow import is from 'ramda/src/is' +import has from 'ramda/src/has' import { uintToInt, intToUint, bytesToHex, - gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' + gzipUncompress, bytesToArrayBuffer, longToInts, lshift32, stringToChars } from '../bin' -import Logger from '../util/log' +import { WriteMediator, ReadMediator } from './mediator' +import Layout, { getFlags, isSimpleType, getTypeProps } from '../layout' +import { TypeBuffer, TypeWriter, getNakedType, + getString, getTypeConstruct } from './type-buffer' +import type { TLSchema, TLConstruct } from './index.h' +import Logger from '../util/log' const debug = Logger`tl` -import { readInt, TypeBuffer, getNakedType, - getPredicate, getString, getTypeConstruct } from './types' - const PACKED = 0x3072cfa1 +type SerialConstruct = { + mtproto: boolean, + startMaxLength: number +} + +let apiLayer: Layout +let mtLayer: Layout + export class Serialization { - maxLength: number - offset: number = 0 // in bytes - buffer: ArrayBuffer - intView: Int32Array - byteView: Uint8Array + writer: TypeWriter = new TypeWriter() mtproto: boolean - constructor({ mtproto, startMaxLength }, api, mtApi) { + api: TLSchema + mtApi: TLSchema + constructor({ mtproto, startMaxLength }: SerialConstruct, api: TLSchema, mtApi: TLSchema) { this.api = api this.mtApi = mtApi - this.maxLength = startMaxLength + this.writer.maxLength = startMaxLength - this.createBuffer() + this.writer.reset() this.mtproto = mtproto - } - - createBuffer() { - this.buffer = new ArrayBuffer(this.maxLength) - this.intView = new Int32Array(this.buffer) - this.byteView = new Uint8Array(this.buffer) - } - - getArray() { - const resultBuffer = new ArrayBuffer(this.offset) - const resultArray = new Int32Array(resultBuffer) - - resultArray.set(this.intView.subarray(0, this.offset / 4)) - - return resultArray - } - - getBuffer() { - return this.getArray().buffer - } - - getBytes(typed) { - if (typed) { - const resultBuffer = new ArrayBuffer(this.offset) - const resultArray = new Uint8Array(resultBuffer) - - resultArray.set(this.byteView.subarray(0, this.offset)) - - return resultArray - } - - const bytes = [] - for (let i = 0; i < this.offset; i++) { - bytes.push(this.byteView[i]) - } - return bytes - } - - checkLength(needBytes) { - if (this.offset + needBytes < this.maxLength) { - return - } - - console.trace('Increase buffer', this.offset, needBytes, this.maxLength) - this.maxLength = Math.ceil(Math.max(this.maxLength * 2, this.offset + needBytes + 16) / 4) * 4 - const previousBuffer = this.buffer - const previousArray = new Int32Array(previousBuffer) - - this.createBuffer() - - new Int32Array(this.buffer).set(previousArray) - } - - writeInt(i, field) { - // this.debug && console.log('>>>', i.toString(16), i, field) - - this.checkLength(4) - this.intView[this.offset / 4] = i - this.offset += 4 - } - - storeIntString = (value, field) => { - switch (true) { - case is(String, value): return this.storeString(value, field) - case is(Number, value): return this.storeInt(value, field) - default: throw new Error(`tl storeIntString field ${field} value type ${typeof value}`) - } - } - - storeInt = (i, field = '') => { - this.writeInt(i, `${ field }:int`) - } - - storeBool(i, field = '') { - if (i) { - this.writeInt(0x997275b5, `${ field }:bool`) - } else { - this.writeInt(0xbc799737, `${ field }:bool`) - } - } - - storeLongP(iHigh, iLow, field) { - this.writeInt(iLow, `${ field }:long[low]`) - this.writeInt(iHigh, `${ field }:long[high]`) - } - - storeLong(sLong, field = '') { - if (is(Array, sLong)) - return sLong.length === 2 - ? this.storeLongP(sLong[0], sLong[1], field) - : this.storeIntBytes(sLong, 64, field) - - if (typeof sLong !== 'string') - sLong = sLong - ? sLong.toString() - : '0' - const [int1, int2] = longToInts(sLong) - this.writeInt(int2, `${ field }:long[low]`) - this.writeInt(int1, `${ field }:long[high]`) - } - - storeDouble(f, field = '') { - const buffer = new ArrayBuffer(8) - const intView = new Int32Array(buffer) - const doubleView = new Float64Array(buffer) - - doubleView[0] = f - - this.writeInt(intView[0], `${ field }:double[low]`) - this.writeInt(intView[1], `${ field }:double[high]`) - } - - storeString(s, field = '') { - // this.debug && console.log('>>>', s, `${ field }:string`) - - if (s === undefined) - s = '' - const sUTF8 = unescape(encodeURIComponent(s)) - - this.checkLength(sUTF8.length + 8) - - const len = sUTF8.length - if (len <= 253) { - this.byteView[this.offset++] = len - } else { - this.byteView[this.offset++] = 254 - this.byteView[this.offset++] = len & 0xFF - this.byteView[this.offset++] = (len & 0xFF00) >> 8 - this.byteView[this.offset++] = (len & 0xFF0000) >> 16 - } - for (let i = 0; i < len; i++) - this.byteView[this.offset++] = sUTF8.charCodeAt(i) - - // Padding - while (this.offset % 4) - this.byteView[this.offset++] = 0 - } - - storeBytes(bytes, field = '') { - if (bytes instanceof ArrayBuffer) { - bytes = new Uint8Array(bytes) - } - else if (bytes === undefined) - bytes = [] - // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:bytes`) - - const len = bytes.byteLength || bytes.length - this.checkLength(len + 8) - if (len <= 253) { - this.byteView[this.offset++] = len - } else { - this.byteView[this.offset++] = 254 - this.byteView[this.offset++] = len & 0xFF - this.byteView[this.offset++] = (len & 0xFF00) >> 8 - this.byteView[this.offset++] = (len & 0xFF0000) >> 16 - } - - this.byteView.set(bytes, this.offset) - this.offset += len - - // Padding - while (this.offset % 4) { - this.byteView[this.offset++] = 0 - } - } - - storeIntBytes(bytes, bits, field = '') { - if (bytes instanceof ArrayBuffer) { - bytes = new Uint8Array(bytes) - } - const len = bytes.length - if (bits % 32 || len * 8 != bits) { - throw new Error(`Invalid bits: ${ bits }, ${ bytes.length}`) + if (!apiLayer) + apiLayer = new Layout(api) + if (!mtLayer) + mtLayer = new Layout(mtApi) + } + + getBytes(typed?: boolean) { + if (typed) + return this.writer.getBytesTyped() + else + return this.writer.getBytesPlain() + } + + storeMethod(methodName: string, params) { + const layer = this.mtproto + ? mtLayer + : apiLayer + const pred = layer.funcs.get(methodName) + if (!pred) throw new Error(`No method name ${methodName} found`) + + WriteMediator.int(this.writer, + intToUint(`${pred.id}`), + `${methodName}[id]`) + if (pred.hasFlags) { + const flags = getFlags(pred)(params) + this.storeObject(flags, '#', `f ${methodName} #flags ${flags}`) } - - // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:int${ bits}`) - this.checkLength(len) - - this.byteView.set(bytes, this.offset) - this.offset += len - } - - storeRawBytes(bytes, field = '') { - if (bytes instanceof ArrayBuffer) { - bytes = new Uint8Array(bytes) - } - const len = bytes.length - - // this.debug && console.log('>>>', bytesToHex(bytes), field) - this.checkLength(len) - - this.byteView.set(bytes, this.offset) - this.offset += len - } - - storeMethod(methodName, params) { - const schema = selectSchema(this.mtproto, this.api, this.mtApi) - let methodData = false - - for (let i = 0; i < schema.methods.length; i++) { - if (schema.methods[i].method == methodName) { - methodData = schema.methods[i] - break + for (const param of pred.params) { + const paramName = param.name + const typeClass = param.typeClass + let fieldObj + if (!has(paramName, params)) { + if (param.isFlag) continue + else if (layer.typeDefaults.has(typeClass)) + fieldObj = layer.typeDefaults.get(typeClass) + else if (isSimpleType(typeClass)) { + switch (typeClass) { + case 'int': fieldObj = 0; break + // case 'long': fieldObj = 0; break + case 'string': fieldObj = ' '; break + // case 'double': fieldObj = 0; break + case 'true': fieldObj = true; break + // case 'bytes': fieldObj = [0]; break + } + } + else throw new Error(`Method ${methodName} did not receive required argument ${paramName}`) + } else { + fieldObj = params[paramName] } + if (param.isVector) { + if (!Array.isArray(fieldObj)) + throw new TypeError(`Vector argument ${paramName} in ${methodName} required Array,` + + //$FlowIssue + ` got ${fieldObj} ${typeof fieldObj}`) + WriteMediator.int(this.writer, 0x1cb5c415, `${paramName}[id]`) + WriteMediator.int(this.writer, fieldObj.length, `${paramName}[count]`) + for (const [ i, elem ] of fieldObj.entries()) + this.storeObject(elem, param.typeClass, `${paramName}[${i}]`) + } else + this.storeObject(fieldObj, param.typeClass, `f ${methodName}(${paramName})`) } - if (!methodData) { - throw new Error(`No method ${ methodName } found`) - } - - this.storeInt(intToUint(methodData.id), `${methodName }[id]`) - - let param, type - let condType + /*let condType let fieldBit - const len = methodData.params.length - for (let i = 0; i < len; i++) { - param = methodData.params[i] - type = param.type + for (const param of methodData.params) { + let type = param.type if (type.indexOf('?') !== -1) { condType = type.split('?') fieldBit = condType[0].split('.') @@ -257,59 +113,58 @@ export class Serialization { } const paramName = param.name const stored = params[paramName] - /*if (!stored) + if (!stored) stored = this.emptyOfType(type, schema) if (!stored) throw new Error(`Method ${methodName}.`+ - ` No value of field ${ param.name } recieved and no Empty of type ${ param.type }`)*/ - this.storeObject(stored, type, `${methodName }[${ paramName }]`) - } + ` No value of field ${ param.name } recieved and no Empty of type ${ param.type }`) + this.storeObject(stored, type, `f ${methodName}(${paramName})`) + }*/ - return methodData.type + return pred.returns } - emptyOfType(ofType, schema) { + /*emptyOfType(ofType, schema: TLSchema) { const resultConstruct = schema.constructors.find( - ({ type, predicate }) => + ({ type, predicate }: TLConstruct) => type === ofType && predicate.indexOf('Empty') !== -1) return resultConstruct ? { _: resultConstruct.predicate } : null - } - storeObject(obj, type, field) { + }*/ + storeObject(obj, type: string, field: string) { switch (type) { case '#': case 'int': - return this.storeInt(obj, field) + return WriteMediator.int(this.writer, obj, field) case 'long': - return this.storeLong(obj, field) + return WriteMediator.long(this.writer, obj, field) case 'int128': - return this.storeIntBytes(obj, 128, field) + return WriteMediator.intBytes(this.writer, obj, 128, field) case 'int256': - return this.storeIntBytes(obj, 256, field) + return WriteMediator.intBytes(this.writer, obj, 256, field) case 'int512': - return this.storeIntBytes(obj, 512, field) + return WriteMediator.intBytes(this.writer, obj, 512, field) case 'string': - return this.storeString(obj, field) + return WriteMediator.bytes(this.writer, obj, `${field}:string`) case 'bytes': - return this.storeBytes(obj, field) + return WriteMediator.bytes(this.writer, obj, field) case 'double': - return this.storeDouble(obj, field) + return WriteMediator.double(this.writer, obj, field) case 'Bool': - return this.storeBool(obj, field) + return WriteMediator.bool(this.writer, obj, field) case 'true': return } - if (is(Array, obj)) { - if (type.substr(0, 6) == 'Vector') { - this.writeInt(0x1cb5c415, `${field }[id]`) - } + if (Array.isArray(obj)) { + if (type.substr(0, 6) == 'Vector') + WriteMediator.int(this.writer, 0x1cb5c415, `${field}[id]`) else if (type.substr(0, 6) != 'vector') { throw new Error(`Invalid vector type ${ type}`) } const itemType = type.substr(7, type.length - 8) // for "Vector" - this.writeInt(obj.length, `${field }[count]`) + WriteMediator.int(this.writer, obj.length, `${field}[count]`) for (let i = 0; i < obj.length; i++) { this.storeObject(obj[i], itemType, `${field }[${ i }]`) } @@ -323,34 +178,37 @@ export class Serialization { throw new Error(`Invalid object for type ${ type}`) const schema = selectSchema(this.mtproto, this.api, this.mtApi) + const predicate = obj['_'] let isBare = false let constructorData = false - - if (isBare = type.charAt(0) == '%') + isBare = type.charAt(0) == '%' + if (isBare) type = type.substr(1) - for (let i = 0; i < schema.constructors.length; i++) { - if (schema.constructors[i].predicate == predicate) { - constructorData = schema.constructors[i] + + for (const tlConst of schema.constructors) { + if (tlConst.predicate == predicate) { + constructorData = tlConst break } } + if (!constructorData) - throw new Error(`No predicate ${ predicate } found`) + throw new Error(`No predicate ${predicate} found`) if (predicate == type) isBare = true if (!isBare) - this.writeInt(intToUint(constructorData.id), `${field }[${ predicate }][id]`) + WriteMediator.int(this.writer, + intToUint(constructorData.id), + `${field}.${predicate}[id]`) - let param let condType let fieldBit - const len = constructorData.params.length - for (let i = 0; i < len; i++) { - param = constructorData.params[i] + + for (const param of constructorData.params) { type = param.type if (type.indexOf('?') !== -1) { condType = type.split('?') @@ -361,7 +219,7 @@ export class Serialization { type = condType[1] } - this.storeObject(obj[param.name], type, `${field }[${ predicate }][${ param.name }]`) + this.storeObject(obj[param.name], type, `${field}.${ predicate }.${ param.name }`) } return constructorData.type @@ -373,7 +231,9 @@ export class Deserialization { typeBuffer: TypeBuffer override: Object mtproto: boolean - constructor(buffer: Buffer, { mtproto, override }: DConfig, api, mtApi) { + api: TLSchema + mtApi: TLSchema + constructor(buffer: Buffer, { mtproto, override }: DConfig, api: TLSchema, mtApi: TLSchema) { this.api = api this.mtApi = mtApi this.override = override @@ -382,44 +242,16 @@ export class Deserialization { this.mtproto = mtproto } - readInt = readInt(this) - - fetchInt(field = '') { - return this.readInt(`${ field }:int`) - } - - fetchDouble(field = '') { - const buffer = new ArrayBuffer(8) - const intView = new Int32Array(buffer) - const doubleView = new Float64Array(buffer) - - intView[0] = this.readInt(`${ field }:double[low]`) - intView[1] = this.readInt(`${ field }:double[high]`) - - return doubleView[0] + readInt = (field: string) => { + // log('int')(field, i.toString(16), i) + return ReadMediator.int(this.typeBuffer, field) } - fetchLong(field = '') { - const iLow = this.readInt(`${ field }:long[low]`) - const iHigh = this.readInt(`${ field }:long[high]`) - - const res = lshift32(iHigh, iLow) - // const longDec = bigint(iHigh) - // .shiftLeft(32) - // .add(bigint(iLow)) - // .toString() - - - // debug`long, iLow, iHigh`(strDecToHex(iLow.toString()), - // strDecToHex(iHigh.toString())) - // debug`long, leemon`(res, strDecToHex(res.toString())) - // debug`long, bigint`(longDec, strDecToHex(longDec.toString())) - - - return res + fetchInt(field: string = '') { + return this.readInt(`${ field }:int`) } - fetchBool(field = '') { + fetchBool(field: string = '') { const i = this.readInt(`${ field }:bool`) switch (i) { case 0x997275b5: return true @@ -430,48 +262,7 @@ export class Deserialization { } } } - - fetchString(field = '') { - let len = this.typeBuffer.nextByte() - - if (len == 254) { - len = this.typeBuffer.nextByte() | - this.typeBuffer.nextByte() << 8 | - this.typeBuffer.nextByte() << 16 - } - - const sUTF8 = getString(len, this.typeBuffer) - - let s - try { - s = decodeURIComponent(escape(sUTF8)) - } catch (e) { - s = sUTF8 - } - - debug(`string`)(s, `${field}:string`) - - return s - } - - fetchBytes(field = '') { - let len = this.typeBuffer.nextByte() - - if (len == 254) { - len = this.typeBuffer.nextByte() | - this.typeBuffer.nextByte() << 8 | - this.typeBuffer.nextByte() << 16 - } - - const bytes = this.typeBuffer.next(len) - this.typeBuffer.addPadding() - - debug(`bytes`)(bytesToHex(bytes), `${ field }:bytes`) - - return bytes - } - - fetchIntBytes(bits, field = '') { + fetchIntBytes(bits: number, field: string = '') { if (bits % 32) throw new Error(`Invalid bits: ${bits}`) @@ -484,7 +275,7 @@ export class Deserialization { return bytes } - fetchRawBytes(len, field = '') { + fetchRawBytes(len: number | false, field: string = '') { if (len === false) { len = this.readInt(`${ field }_length`) if (len > this.typeBuffer.byteView.byteLength) @@ -496,16 +287,22 @@ export class Deserialization { return bytes } - fetchPacked(type, field) { - const compressed = this.fetchBytes(`${field}[packed_string]`) + fetchPacked(type, field: string = '') { + const compressed = ReadMediator.bytes( this.typeBuffer, `${field}[packed_string]`) const uncompressed = gzipUncompress(compressed) const buffer = bytesToArrayBuffer(uncompressed) - const newDeserializer = new Deserialization(buffer, { mtproto: this.mtproto, override: this.override }, this.api, this.mtApi) + const newDeserializer = new Deserialization( + buffer, { + mtproto : this.mtproto, + override: this.override + }, + this.api, this.mtApi) return newDeserializer.fetchObject(type, field) } - fetchVector(type, field) { + fetchVector(type: string, field: string = '') { + const typeProps = getTypeProps(type) if (type.charAt(0) === 'V') { const constructor = this.readInt(`${field}[id]`) const constructorCmp = uintToInt(constructor) @@ -526,13 +323,13 @@ export class Deserialization { return result } - fetchObject(type, field) { + fetchObject(type, field: string = '') { switch (type) { case '#': case 'int': return this.fetchInt(field) case 'long': - return this.fetchLong(field) + return ReadMediator.long(this.typeBuffer, field) case 'int128': return this.fetchIntBytes(128, field) case 'int256': @@ -540,11 +337,11 @@ export class Deserialization { case 'int512': return this.fetchIntBytes(512, field) case 'string': - return this.fetchString(field) + return ReadMediator.string(this.typeBuffer, field) case 'bytes': - return this.fetchBytes(field) + return ReadMediator.bytes(this.typeBuffer, field) case 'double': - return this.fetchDouble(field) + return ReadMediator.double(this.typeBuffer, field) case 'Bool': return this.fetchBool(field) case 'true': @@ -552,17 +349,22 @@ export class Deserialization { } let fallback field = field || type || 'Object' - if (type.substr(0, 6).toLowerCase() === 'vector') + + // const layer = this.mtproto + // ? mtLayer + // : apiLayer + const typeProps = getTypeProps(type) + // layer.typesById + + if (typeProps.isVector) return this.fetchVector(type, field) const schema = selectSchema(this.mtproto, this.api, this.mtApi) let predicate = false let constructorData = false - if (type.charAt(0) === '%') + if (typeProps.isBare) constructorData = getNakedType(type, schema) - else if (type.charAt(0) >= 97 && type.charAt(0) <= 122) - constructorData = getPredicate(type, schema) else { const constructor = this.readInt(`${field}[id]`) const constructorCmp = uintToInt(constructor) @@ -603,9 +405,7 @@ export class Deserialization { if (this.override[overrideKey]) { this.override[overrideKey].apply(this, [result, `${field}[${predicate}]`]) } else { - const len = constructorData.params.length - for (let i = 0; i < len; i++) { - const param = constructorData.params[i] + for (const param of constructorData.params) { type = param.type // if (type === '#' && isNil(result.pFlags)) // result.pFlags = {} @@ -641,7 +441,7 @@ export class Deserialization { } -const selectSchema = (mtproto: boolean, api, mtApi) => mtproto +const selectSchema = (mtproto: boolean, api: TLSchema, mtApi: TLSchema) => mtproto ? mtApi : api @@ -668,11 +468,13 @@ export type TLFabric = { Deserialization: DeserializationFabric } -export const TL = (api, mtApi) => ({ +export const TL = (api: TLSchema, mtApi: TLSchema) => ({ Serialization: ({ mtproto = false, startMaxLength = 2048 /* 2Kb */ } = {}) => new Serialization({ mtproto, startMaxLength }, api, mtApi), Deserialization: (buffer: Buffer, { mtproto = false, override = {} }: DConfig = {}) => new Deserialization(buffer, { mtproto, override }, api, mtApi) }) -export default TL \ No newline at end of file +export * from './mediator' +export { TypeWriter } from './type-buffer' +export default TL diff --git a/src/tl/mediator.js b/src/tl/mediator.js new file mode 100644 index 0000000..e58ca99 --- /dev/null +++ b/src/tl/mediator.js @@ -0,0 +1,179 @@ +//@flow + +import { TypeWriter, TypeBuffer } from './type-buffer' +import { longToInts, stringToChars, lshift32, bytesToHex } from '../bin' + +import Logger from '../util/log' +const log = Logger`tl:mediator` + +import type { BinaryData } from './index.h' + +export const WriteMediator = { + int(ctx: TypeWriter, i: number, field: string = '') { + ctx.writeInt(i, `${ field }:int`) + }, + bool(ctx: TypeWriter, i: boolean, field: string = '') { + if (i) { + ctx.writeInt(0x997275b5, `${ field }:bool`) + } else { + ctx.writeInt(0xbc799737, `${ field }:bool`) + } + }, + longP(ctx: TypeWriter, + iHigh: number, + iLow: number, + field: string) { + ctx.writePair(iLow, iHigh, + `${ field }:long[low]`, + `${ field }:long[high]`) + }, + long(ctx: TypeWriter, + sLong?: number[] | string | number, + field: string = '') { + if (Array.isArray(sLong)) + return sLong.length === 2 + ? this.longP(ctx, sLong[0], sLong[1], field) + : this.intBytes(ctx, sLong, 64, field) + let str + if (typeof sLong !== 'string') + str = sLong + ? sLong.toString() + : '0' + else str = sLong + const [int1, int2] = longToInts(str) + ctx.writePair(int2, int1, + `${ field }:long[low]`, + `${ field }:long[high]`) + }, + double(ctx: TypeWriter, f: number, field: string = '') { + const buffer = new ArrayBuffer(8) + const intView = new Int32Array(buffer) + const doubleView = new Float64Array(buffer) + + doubleView[0] = f + + const [int1, int2] = intView + ctx.writePair(int2, int1, + `${ field }:double[low]`, + `${ field }:double[high]`) + }, + bytes(ctx: TypeWriter, + bytes?: number[] | ArrayBuffer | string, + field: string = '') { + const { list, length } = binaryDataGuard(bytes) + // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:bytes`) + + ctx.checkLength(length + 8) + if (length <= 253) { + ctx.next(length) + } else { + ctx.next(254) + ctx.next(length & 0xFF) + ctx.next((length & 0xFF00) >> 8) + ctx.next((length & 0xFF0000) >> 16) + } + + ctx.set(list, length) + ctx.addPadding() + }, + intBytes(ctx: TypeWriter, + bytes: BinaryData | ArrayBuffer | string, + bits: number | false, + field: string = '') { + const { list, length } = binaryDataGuard(bytes) + + if (bits) { + if (bits % 32 || length * 8 != bits) { + throw new Error(`Invalid bits: ${ bits }, ${length}`) + } + } + // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:int${ bits}`) + ctx.checkLength(length) + ctx.set(list, length) + } +} + +export const ReadMediator = { + int(ctx: TypeBuffer, field: string) { + const result = ctx.nextInt() + log('read, int')(field, result) + return result + }, + long(ctx: TypeBuffer, field: string ){ + const iLow = this.int(ctx, `${ field }:long[low]`) + const iHigh = this.int(ctx, `${ field }:long[high]`) + + const res = lshift32(iHigh, iLow) + return res + }, + double(ctx: TypeBuffer, field: string) { + const buffer = new ArrayBuffer(8) + const intView = new Int32Array(buffer) + const doubleView = new Float64Array(buffer) + + intView[0] = this.int(ctx, `${ field }:double[low]`) + intView[1] = this.int(ctx, `${ field }:double[high]`) + + return doubleView[0] + }, + string(ctx: TypeBuffer, field: string) { + const bytes = this.bytes(ctx, `${field}:string`) + const sUTF8 = [...bytes] + .map(getChar) + .join('') + + let s + try { + s = decodeURIComponent(escape(sUTF8)) + } catch (e) { + s = sUTF8 + } + + log(`read, string`)(s, `${field}:string`) + + return s + }, + bytes(ctx: TypeBuffer, field: string) { + let len = ctx.nextByte() + + if (len == 254) { + len = ctx.nextByte() | + ctx.nextByte() << 8 | + ctx.nextByte() << 16 + } + + const bytes = ctx.next(len) + ctx.addPadding() + + log(`read, bytes`)(bytesToHex(bytes), `${ field }:bytes`) + + return bytes + } +} + +const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) => { + let list, length + if (bytes instanceof ArrayBuffer) { + list = new Uint8Array(bytes) + length = bytes.byteLength + } else if (typeof bytes === 'string') { + list = + stringToChars( + unescape( + encodeURIComponent( + bytes))) + length = list.length + } else if (bytes === undefined) { + list = [] + length = 0 + } else { + list = bytes + length = bytes.length + } + return { + list, + length + } +} + +const getChar = (e: number) => String.fromCharCode(e) diff --git a/src/tl/type-buffer.js b/src/tl/type-buffer.js new file mode 100644 index 0000000..29eb4c0 --- /dev/null +++ b/src/tl/type-buffer.js @@ -0,0 +1,214 @@ +//@flow + +import isNode from 'detect-node' + +import Logger from '../util/log' +const log = Logger('tl', 'type-buffer') + +import { immediate } from '../util/smart-timeout' +import { TypeBufferIntError } from '../error' + +// import { bigint, uintToInt, intToUint, bytesToHex, +// gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' + +import type { BinaryData, TLConstruct, TLSchema } from './index.h' + +function findType(val: TLConstruct) { + return val.type == this +} + +function findPred(val: TLConstruct) { + return val.predicate == this +} + +function findId(val: TLConstruct) { + return val.id == this +} + +export const getNakedType = (type: string, schema: TLSchema) => { + const checkType = type.substr(1) + const result = schema.constructors.find(findType, checkType) + if (!result) + throw new Error(`Constructor not found for type: ${type}`) + return result +} + +export const getPredicate = (type: string, schema: TLSchema) => { + const result = schema.constructors.find(findPred, type) + if (!result) + throw new Error(`Constructor not found for predicate: ${type}`) + return result +} + +export const getTypeConstruct = + (construct: number, schema: TLSchema) => + schema.constructors.find(findId, construct) + +const getChar = (e: number) => String.fromCharCode(e) + +export const getString = (length: number, buffer: TypeBuffer) => { + const bytes = buffer.next(length) + + const result = [...bytes].map(getChar).join('') + buffer.addPadding() + return result +} + +const countNewLength = (maxLength: number, need: number, offset: number) => { + const e1 = maxLength * 2 + const e2 = offset + need + 16 + const max = Math.max(e1, e2) / 4 + const rounded = Math.ceil(max) * 4 + return rounded +} + +const writeIntLogger = log('writeInt') + +const writeIntLog = (i: number, field: string) => { + const hex = i && i.toString(16) || 'UNDEF' + writeIntLogger(hex, i, field) +} + +export class TypeWriter { + offset: number = 0 // in bytes + buffer: ArrayBuffer + intView: Int32Array + byteView: Uint8Array + maxLength: number + constructor(/*startMaxLength: number*/) { + // this.maxLength = startMaxLength + // this.reset() + } + reset() { + this.buffer = new ArrayBuffer(this.maxLength) + this.intView = new Int32Array(this.buffer) + this.byteView = new Uint8Array(this.buffer) + } + set(list: BinaryData, length: number) { + this.byteView.set(list, this.offset) + this.offset += length + } + next(data: number) { + this.byteView[this.offset] = data + this.offset++ + } + checkLength(needBytes: number) { + if (this.offset + needBytes < this.maxLength) { + return + } + log('Increase buffer')(this.offset, needBytes, this.maxLength) + this.maxLength = countNewLength( + this.maxLength, + needBytes, + this.offset + ) + const previousBuffer = this.buffer + const previousArray = new Int32Array(previousBuffer) + + this.reset() + + new Int32Array(this.buffer).set(previousArray) + } + getArray() { + const resultBuffer = new ArrayBuffer(this.offset) + const resultArray = new Int32Array(resultBuffer) + + resultArray.set(this.intView.subarray(0, this.offset / 4)) + + return resultArray + } + getBuffer() { + return this.getArray().buffer + } + getBytesTyped() { + const resultBuffer = new ArrayBuffer(this.offset) + const resultArray = new Uint8Array(resultBuffer) + + resultArray.set(this.byteView.subarray(0, this.offset)) + + return resultArray + } + getBytesPlain() { + const bytes = [] + for (let i = 0; i < this.offset; i++) { + bytes.push(this.byteView[i]) + } + return bytes + } + writeInt(i: number, field: string) { + immediate(writeIntLog, i, field) + + this.checkLength(4) + this.intView[this.offset / 4] = i + this.offset += 4 + } + writePair(n1: number, n2: number, field1: string, field2: string) { + this.writeInt(n1, field1) + this.writeInt(n2, field2) + } + addPadding() { + while (this.offset % 4) + this.next(0) + } +} + +export class TypeBuffer { + offset: number = 0 + buffer: Buffer + intView: Uint32Array + byteView: Uint8Array + constructor(buffer: Buffer) { + this.buffer = buffer + this.intView = toUint32(buffer) + this.byteView = new Uint8Array(buffer) + } + + nextByte() { + return this.byteView[this.offset++] + } + nextInt() { + if (this.offset >= this.intView.length * 4) + throw new TypeBufferIntError(this) + const int = this.intView[this.offset / 4] + this.offset += 4 + return int + } + readPair(field1: string, field2: string) { + const int1 = this.nextInt(field1) + const int2 = this.nextInt(field2) + return [ int1, int2 ] + } + next(length: number) { + const result = this.byteView.subarray(this.offset, this.offset + length) + this.offset += length + return result + } + isEnd() { + return this.offset === this.byteView.length + } + addPadding() { + const offset = this.offset % 4 + if (offset > 0) + this.offset += 4 - offset + } +} + +const toUint32 = (buf: Buffer) => { + let ln, res + if (!isNode) //TODO browser behavior not equals, why? + return new Uint32Array( buf ) + if (buf.readUInt32LE) { + ln = buf.byteLength / 4 + res = new Uint32Array( ln ) + for (let i = 0; i < ln; i++) + res[i] = buf.readUInt32LE( i*4 ) + } else { + //$FlowIssue + const data = new DataView( buf ) + ln = data.byteLength / 4 + res = new Uint32Array( ln ) + for (let i = 0; i < ln; i++) + res[i] = data.getUint32( i*4, true ) + } + return res +} diff --git a/src/tl/types.js b/src/tl/types.js deleted file mode 100644 index 30f83d1..0000000 --- a/src/tl/types.js +++ /dev/null @@ -1,131 +0,0 @@ -//@flow - -import isNode from 'detect-node' -import Debug from 'debug' - -const debug = Debug('telegram-mtproto:tl:types') - -import { TypeBufferIntError } from '../error' - -// import { bigint, uintToInt, intToUint, bytesToHex, -// gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' - -type TLParam = { - name: string, - type: string -} - -type TLConstruct = { - id: string, - type: string, - predicate: string, - param: TLParam[] -} - -export type TLSchema = { - constructors: TLConstruct[], - methods: any[], - constructorsIndex?: number[] -} - -export const readInt = (ctx: any) => (field: string) => { - const i = ctx.typeBuffer.nextInt() - debug('[int]', field, i.toString(16), i) - return i -} - -function findType(val: TLConstruct) { - return val.type == this -} - -function findPred(val: TLConstruct) { - return val.predicate == this -} - -function findId(val: TLConstruct) { - return val.id == this -} - -export const getNakedType = (type: string, schema: TLSchema) => { - const checkType = type.substr(1) - const result = schema.constructors.find(findType, checkType) - if (!result) - throw new Error(`Constructor not found for type: ${type}`) - return result -} - -export const getPredicate = (type: string, schema: TLSchema) => { - const result = schema.constructors.find(findPred, type) - if (!result) - throw new Error(`Constructor not found for predicate: ${type}`) - return result -} - -export const getTypeConstruct = - (construct: number, schema: TLSchema) => - schema.constructors.find(findId, construct) - -const getChar = (e: number) => String.fromCharCode(e) - -export const getString = (length: number, buffer: TypeBuffer) => { - const bytes = buffer.next(length) - - const result = [...bytes].map(getChar).join('') - buffer.addPadding() - return result -} - -export class TypeBuffer { - offset: number = 0 - buffer: Buffer - intView: Uint32Array - byteView: Uint8Array - constructor(buffer: Buffer) { - this.buffer = buffer - this.intView = toUint32(buffer) - this.byteView = new Uint8Array(buffer) - } - nextByte() { - return this.byteView[this.offset++] - } - nextInt() { - if (this.offset >= this.intView.length * 4) - throw new TypeBufferIntError(this) - const int = this.intView[this.offset / 4] - this.offset += 4 - return int - } - next(length: number) { - const result = this.byteView.subarray(this.offset, this.offset + length) - this.offset += length - return result - } - isEnd() { - return this.offset === this.byteView.length - } - addPadding() { - const offset = this.offset % 4 - if (offset > 0) - this.offset += 4 - offset - } -} - -const toUint32 = (buf: Buffer) => { - let ln, res - if (!isNode) //TODO browser behavior not equals, why? - return new Uint32Array( buf ) - if (buf.readUInt32LE) { - ln = buf.byteLength / 4 - res = new Uint32Array( ln ) - for (let i = 0; i < ln; i++) - res[i] = buf.readUInt32LE( i*4 ) - } else { - //$FlowIssue - const data = new DataView( buf ) - ln = data.byteLength / 4 - res = new Uint32Array( ln ) - for (let i = 0; i < ln; i++) - res[i] = data.getUint32( i*4, true ) - } - return res -} diff --git a/src/util/dtime.js b/src/util/dtime.js new file mode 100644 index 0000000..2d16cdf --- /dev/null +++ b/src/util/dtime.js @@ -0,0 +1,7 @@ +//@flow + +const logTimer = (new Date()).getTime() + +export const dTime = () => `[${(((new Date()).getTime() -logTimer) / 1000).toFixed(3)}]` + +export default dTime \ No newline at end of file diff --git a/src/util/log.js b/src/util/log.js index f680731..8147bfe 100644 --- a/src/util/log.js +++ b/src/util/log.js @@ -1,5 +1,7 @@ //@flow +// import memoize from 'memoizee' + import Debug from 'debug' import trim from 'ramda/src/trim' @@ -18,7 +20,8 @@ import unapply from 'ramda/src/unapply' import unnest from 'ramda/src/unnest' import tail from 'ramda/src/tail' -import { dTime } from '../service/time-manager' +import dTime from './dtime' +import { immediate } from './smart-timeout' type VariString = string | string[] @@ -40,11 +43,62 @@ const fullNormalize: FullNormalize = pipe( const stringNormalize = when( both(is(String), e => e.length > 50), - take(50) + take(150) ) +// const isSimple = either( +// is(String), +// is(Number) +// ) + +// const prettify = unless( +// isSimple, +// pretty +// ) + +const genericLogger = Debug('telegram-mtproto') + +class LogEvent { + log: typeof genericLogger + values: mixed[] + constructor(log: typeof genericLogger, values: mixed[]) { + this.log = log + this.values = values + } + print() { + this.log(...this.values) + } +} + +class Sheduler { + queue: LogEvent[][] = [] + buffer: LogEvent[] = [] + add = (log: typeof genericLogger, time: string, tagStr: string, values: mixed[]) => { + const results = values.map(stringNormalize) + const first = results[0] || '' + const other = tail(results) + const firstLine = [tagStr, time, first].join(' ') + this.buffer.push(new LogEvent(log, [firstLine, ...other])) + } + sheduleBuffer = () => { + this.queue.push(this.buffer) + this.buffer = [] + } + print = () => { + for (const buffer of this.queue) + for (const logEvent of buffer) + logEvent.print() + this.queue = [] + } + constructor() { + setInterval(this.sheduleBuffer, 50) + setInterval(this.print, 300) + } +} + +const sheduler = new Sheduler -const Logger = (moduleName: VariString, ...thanksFlow: string[]) => { - const fullModule: string[] = arrify(moduleName, ...thanksFlow) +const Logger = (moduleName: VariString, ...rest: string[]) => { + const fullModule: string[] = arrify(moduleName, ...rest) fullModule.unshift('telegram-mtproto') const fullname = fullModule.join(':') const debug = Debug(fullname) @@ -52,14 +106,14 @@ const Logger = (moduleName: VariString, ...thanksFlow: string[]) => { const tagStr = fullNormalize(tags) return (...objects: any[]) => { const time = dTime() - const results = objects.map(stringNormalize) - const first = results[0] || '' - const other = tail(results) - const firstLine = [tagStr, time, first].join(' ') - setTimeout(debug, 200, firstLine, ...other) + immediate(sheduler.add, debug, time, tagStr, objects) } } return logger } +export const setLogger = (customLogger: Function) => { + Debug.log = customLogger +} + export default Logger \ No newline at end of file diff --git a/test/layout.test.js b/test/layout.test.js new file mode 100644 index 0000000..99db44f --- /dev/null +++ b/test/layout.test.js @@ -0,0 +1,183 @@ +const { test } = require('tap') +const { getFlags, Layout } = require('../lib/layout') +const apiSchema = require('../schema/api-57.json') + +const { has } = require('ramda') + +const methodRaw = { + 'id' : '-1137057461', + 'method': 'messages.saveDraft', + 'params': [{ + 'name': 'flags', + 'type': '#' + }, { + 'name': 'no_webpage', + 'type': 'flags.1?true' + }, { + 'name': 'reply_to_msg_id', + 'type': 'flags.0?int' + }, { + 'name': 'peer', + 'type': 'InputPeer' + }, { + 'name': 'message', + 'type': 'string' + }, { + 'name': 'entities', + 'type': 'flags.3?Vector' + }], + 'type': 'Bool' +} + + +const methodModel = { + id : -1137057461, + name : 'messages.saveDraft', + returns : 'Bool', + hasFlags: true, + params : [ + { + name : 'flags', + type : '#', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'no_webpage', + type : 'true', + isVector : false, + isFlag : true, + flagIndex: 1 + }, { + name : 'reply_to_msg_id', + type : 'int', + isVector : false, + isFlag : true, + flagIndex: 0 + }, { + name : 'peer', + type : 'InputPeer', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'message', + type : 'string', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'entities', + type : 'MessageEntity', + isVector : true, + isFlag : true, + flagIndex: 3 + }, + ] +} + +const MessageEntity = { + name : 'MessageEntity', + creators: [ + 'messageEntityUnknown', + 'messageEntityMention', + 'messageEntityHashtag' + ] +} + +const entityCreator1 = { + 'id' : '-1148011883', + 'predicate': 'messageEntityUnknown', + 'params' : [ + { + 'name': 'offset', + 'type': 'int' + }, { + 'name': 'length', + 'type': 'int' + } + ], + 'type': 'MessageEntity' +} +const entityCreator2 = { + 'id' : '-100378723', + 'predicate': 'messageEntityMention', + 'params' : [ + { + 'name': 'offset', + 'type': 'int' + }, { + 'name': 'length', + 'type': 'int' + } + ], + 'type': 'MessageEntity' +} +const entityCreator3 = { + 'id' : '1868782349', + 'predicate': 'messageEntityHashtag', + 'params' : [ + { + 'name': 'offset', + 'type': 'int' + }, { + 'name': 'length', + 'type': 'int' + } + ], + 'type': 'MessageEntity' +} + + +const entityCreatorModel = { + id : -1148011883, + name : 'messageEntityUnknown', + type : 'MessageEntity', + hasFlags: false, + params : [ + { + name : 'offset', + type : 'int', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'length', + type : 'int', + isVector : false, + isFlag : false, + flagIndex: NaN + }, + ] +} + +/*const method = Layout.method('auth.sendCode') +for (const param of method.params) { + +}*/ +test('flags counter', t => { + const flagTests = [ + [{}, 0], + [{ + no_webpage: true + }, 2], + [{ + no_webpage : true, + reply_to_msg_id: 1234556 + }, 3], + [{ + entities : [{}], + reply_to_msg_id: 1234556 + }, 9], + ] + + + let getter + t.notThrow(() => getter = getFlags(methodModel), 'getFlags') + for (const [ obj, result ] of flagTests) + t.equal(getter(obj), result, `flags test`) + let layout + t.notThrow(() => layout = new Layout(apiSchema), 'make new layout') + // console.log(layout) + t.end() +}) \ No newline at end of file diff --git a/test/node.test.js b/test/node.test.js index cfc3745..df5468c 100644 --- a/test/node.test.js +++ b/test/node.test.js @@ -1,4 +1,4 @@ -const test = require('tap').test +const { test } = require('tap') const { MTProto } = require('../lib') const phone = { @@ -7,12 +7,12 @@ const phone = { } const api = { - invokeWithLayer: 0xda9b0d0d, - layer : 57, - initConnection : 0x69796de9, - api_id : 49631, - app_version : '1.0.1', - lang_code : 'en' + // invokeWithLayer: 0xda9b0d0d, + layer : 57, + initConnection: 0x69796de9, + api_id : 49631, + app_version : '1.0.1', + lang_code : 'en' } const server = { dev : true,