瀏覽代碼

Add type validation for raw API requests

painor 4 年之前
父節點
當前提交
466eb56ca8
共有 10 個文件被更改,包括 134 次插入36 次删除
  1. 5 0
      gramjs/Helpers.ts
  2. 0 1
      gramjs/Utils.ts
  3. 1 1
      gramjs/client/buttons.ts
  4. 2 2
      gramjs/client/messages.ts
  5. 2 2
      gramjs/client/users.ts
  6. 2 1
      gramjs/entityCache.ts
  7. 1 1
      gramjs/events/common.ts
  8. 2 1
      gramjs/sessions/Memory.ts
  9. 118 26
      gramjs/tl/api.js
  10. 1 1
      package.json

+ 5 - 0
gramjs/Helpers.ts

@@ -35,6 +35,11 @@ export function escapeRegex(string: string) {
     return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
 }
 
+/**
+ * Helper to find if a given object is an array (or similar)
+ */
+export const isArrayLike = (<T>(x: any): x is ArrayLike<T> => x && typeof x.length === 'number' && typeof x !== 'function' && typeof x!=='string');
+
 /*
 export function addSurrogate(text: string) {
     let temp = "";

+ 0 - 1
gramjs/Utils.ts

@@ -916,7 +916,6 @@ export function getPeer(peer: EntityLike) {
     _raiseCastFail(peer, 'peer')
 }
 
-export const isArrayLike = (<T>(x: any): x is ArrayLike<T> => x && typeof x.length === 'number' && typeof x !== 'function');
 
 export function sanitizeParseMode(mode: string | ParseInterface): ParseInterface {
     if (mode === "md" || mode === "markdown") {

+ 1 - 1
gramjs/client/buttons.ts

@@ -2,7 +2,7 @@ import {Api} from "../tl";
 import type {ButtonLike, MarkupLike} from "../define";
 import {Button} from "../tl/custom/button";
 import {MessageButton} from "../tl/custom/messageButton";
-import {isArrayLike} from "../Utils";
+import {isArrayLike} from "../Helpers";
 
 // ButtonMethods
 export function buildReplyMarkup(buttons: Api.TypeReplyMarkup | undefined | ButtonLike | ButtonLike[] | ButtonLike[][], inlineOnly: boolean = false): Api.TypeReplyMarkup | undefined {

+ 2 - 2
gramjs/client/messages.ts

@@ -2,8 +2,8 @@ import {Api} from "../tl";
 import type {Message} from '../tl/custom/message';
 import type {DateLike, EntityLike, FileLike, MarkupLike, MessageIDLike, MessageLike} from "../define";
 import {RequestIter} from "../requestIter";
-import {_EntityType, _entityType, TotalList} from "../Helpers";
-import {getMessageId, getPeerId, isArrayLike} from "../Utils";
+import {_EntityType, _entityType, TotalList,isArrayLike} from "../Helpers";
+import {getMessageId, getPeerId} from "../Utils";
 import type {TelegramClient} from "../";
 import {utils} from "../";
 

+ 2 - 2
gramjs/client/users.ts

@@ -1,7 +1,7 @@
 import {Api} from "../tl";
 import type {Entity, EntityLike} from "../define";
-import {getPeerId as peerUtils, isArrayLike} from "../Utils";
-import {_entityType, _EntityType, sleep} from "../Helpers";
+import {getPeerId as peerUtils } from "../Utils";
+import {_entityType, _EntityType, sleep,isArrayLike} from "../Helpers";
 import {errors, utils} from "../";
 import type {TelegramClient} from "../";
 import bigInt from 'big-integer';

+ 2 - 1
gramjs/entityCache.ts

@@ -1,6 +1,7 @@
 // Which updates have the following fields?
 
-import {getInputPeer, getPeerId, isArrayLike} from "./Utils";
+import {getInputPeer, getPeerId, } from "./Utils";
+import {isArrayLike} from './Helpers'
 import {Api} from "./tl";
 
 export class EntityCache {

+ 1 - 1
gramjs/events/common.ts

@@ -4,7 +4,7 @@ import {ChatGetter} from "../tl/custom/chatGetter";
 import type {TelegramClient} from "../client/TelegramClient";
 
 import bigInt from "big-integer";
-import {isArrayLike} from "../Utils";
+import {isArrayLike} from "../Helpers";
 import {utils} from "../";
 import {Message} from "../tl/patched";
 

+ 2 - 1
gramjs/sessions/Memory.ts

@@ -3,7 +3,8 @@ import type {AuthKey} from "../crypto/AuthKey";
 import {Api} from "../tl";
 import bigInt from "big-integer";
 
-import {getDisplayName, getInputPeer, getPeerId, isArrayLike} from "../Utils";
+import {getDisplayName, getInputPeer, getPeerId} from "../Utils";
+import {isArrayLike} from "../Helpers";
 import {utils} from "../";
 import type {EntityLike} from "../define";
 

+ 118 - 26
gramjs/tl/api.js

@@ -1,4 +1,7 @@
-const { generateRandomBytes, readBigIntFromBuffer } = require('../Helpers')
+const bigInt = require('big-integer')
+
+
+const { generateRandomBytes, readBigIntFromBuffer, isArrayLike } = require('../Helpers')
 
 
 function generateRandomBigInt() {
@@ -40,6 +43,23 @@ const AUTO_CASTS = new Set([
     'InputChatPhoto',
 ])
 
+class CastError extends Error {
+    constructor(objectName, expected, actual, ...params) {
+        // Pass remaining arguments (including vendor specific ones) to parent constructor
+        const message = 'Found wrong type for ' + objectName + '. expected ' + expected + ' but received ' + actual
+        super(message, ...params)
+
+        // Maintains proper stack trace for where our error was thrown (only available on V8)
+        if (Error.captureStackTrace) {
+            Error.captureStackTrace(this, CastError)
+        }
+
+        this.name = 'CastError'
+        // Custom debugging information
+    }
+}
+
+
 const CACHING_SUPPORTED = typeof self !== 'undefined' && self.localStorage !== undefined
 
 const CACHE_KEY = 'GramJs:apiCache'
@@ -57,11 +77,8 @@ function buildApiFromTlSchema() {
             localStorage.setItem(CACHE_KEY, JSON.stringify(definitions))
         }
     }
+    return createClasses('all', definitions)
 
-    return mergeWithNamespaces(
-        createClasses('constructor', definitions.constructors),
-        createClasses('request', definitions.requests),
-    )
 }
 
 function loadFromCache() {
@@ -74,22 +91,9 @@ function loadFromTlSchemas() {
     const [constructorParamsSchema, functionParamsSchema] = extractParams(schemeContent)
     const constructors = [].concat(constructorParamsApi, constructorParamsSchema)
     const requests = [].concat(functionParamsApi, functionParamsSchema)
-
-    return { constructors, requests }
+    return [].concat(constructors, requests)
 }
 
-function mergeWithNamespaces(obj1, obj2) {
-    const result = { ...obj1 }
-
-    Object.keys(obj2).forEach(key => {
-        if (typeof obj2[key] === 'function' || !result[key]) {
-            result[key] = obj2[key]
-        } else {
-            Object.assign(result[key], obj2[key])
-        }
-    })
-    return result
-}
 
 function extractParams(fileContent) {
     const f = parseTl(fileContent, 109)
@@ -208,24 +212,48 @@ function getArgFromReader(reader, arg) {
     }
 }
 
+function compareType(value, type) {
+    let correct = true
+    switch (type) {
+    case 'number':
+    case 'string':
+    case 'boolean':
+        correct = typeof value === type
+        break
+    case 'bigInt':
+        correct = bigInt.isInstance(value)
+        break
+    case 'true':
+        correct = value
+    case 'buffer':
+        correct = Buffer.isBuffer(value)
+        break
+    case 'date':
+        correct = (value && Object.prototype.toString.call(value) === '[object Date]' && !isNaN(value)) || typeof value === 'number'
+        break
+    default:
+        throw new Error('Unknown type.' + type)
+    }
+    return correct
+}
+
 
 function createClasses(classesType, params) {
     const classes = {}
     for (const classParams of params) {
-        const { name, constructorId, subclassOfId, argsConfig, namespace, result } = classParams
+        const { name, constructorId, subclassOfId, argsConfig, namespace, isFunction, result } = classParams
         const fullName = [namespace, name].join('.').replace(/^\./, '')
 
-
         class VirtualClass {
             static CONSTRUCTOR_ID = constructorId
             static SUBCLASS_OF_ID = subclassOfId
             static className = fullName
-            static classType = classesType
+            static classType = isFunction ? 'request' : 'constructor'
 
             CONSTRUCTOR_ID = constructorId
             SUBCLASS_OF_ID = subclassOfId
             className = fullName
-            classType = classesType
+            classType = isFunction ? 'request' : 'constructor'
 
             constructor(args) {
                 args = args || {}
@@ -266,8 +294,72 @@ function createClasses(classesType, params) {
                 return new this(args)
             }
 
+            validate() {
+                for (const arg in argsConfig) {
+                    if (argsConfig.hasOwnProperty(arg)) {
+                        const currentValue = this[arg]
+                        this.assertType(arg, argsConfig[arg], currentValue)
+
+
+                    }
+                }
+            }
+
+            assertType(objectName, object, value) {
+                let expected
+                if (object['isVector']) {
+                    if (!isArrayLike(value)) {
+                        throw new CastError(objectName, 'array', value)
+                    }
+                    for (const o of value) {
+                        this.assertType(objectName, { ...object, isVector: false }, o)
+                    }
+                } else {
+
+
+                    switch (object['type']) {
+                    case 'int':
+                        expected = 'number'
+                        break
+                    case 'long':
+                    case 'int128':
+                    case 'int256':
+                    case 'double':
+                        expected = 'bigInt'
+                        break
+                    case 'string':
+                        expected = 'string'
+                        break
+                    case 'Bool':
+                        expected = 'boolean'
+                        break
+                    case 'true':
+                        expected = 'true'
+                        break
+                    case 'bytes':
+                        expected = 'buffer'
+                        break
+                    case 'date':
+                        expected = 'date'
+                        break
+                    default:
+                        expected = 'object'
+                    }
+                    if (expected === 'object') {
+                        // will be validated in get byte();
+
+                    } else {
+                        const isCorrectType = compareType(value, expected)
+                        if (isCorrectType !== true) {
+                            throw new CastError(objectName, expected, value)
+                        }
+                    }
+                }
+            }
+
             getBytes() {
-                // The next is pseudo-code:
+                this.validate()
+
                 const idForBytes = this.CONSTRUCTOR_ID
                 const c = Buffer.alloc(4)
                 c.writeUInt32LE(idForBytes, 0)
@@ -323,7 +415,7 @@ function createClasses(classesType, params) {
             }
 
             readResult(reader) {
-                if (classesType !== 'request') {
+                if (!isFunction) {
                     throw new Error('`readResult()` called for non-request instance')
                 }
 
@@ -348,7 +440,7 @@ function createClasses(classesType, params) {
             }
 
             async resolve(client, utils) {
-                if (classesType !== 'request') {
+                if (!isFunction) {
                     throw new Error('`resolve()` called for non-request instance')
                 }
                 for (const arg in argsConfig) {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "telegram",
-  "version": "1.5.1",
+  "version": "1.5.5",
   "description": "NodeJS MTProto API Telegram client library,",
   "main": "index.js",
   "types": "index.d.ts",