fix: add legacy constants for decoding

This commit is contained in:
Adhiraj Singh
2022-04-24 16:49:13 +05:30
parent a7e9dcf512
commit 3c278b35f0
7 changed files with 239 additions and 385 deletions

View File

@@ -10,7 +10,7 @@ import { createSignalIdentity } from './signal'
type ClientPayloadConfig = Pick<SocketConfig, 'version' | 'browser'>
const getUserAgent = ({ version, browser }: ClientPayloadConfig): proto.IUserAgent => {
const getUserAgent = ({ version }: ClientPayloadConfig): proto.IUserAgent => {
const osVersion = '0.1'
return {
appVersion: {

View File

@@ -0,0 +1,205 @@
export const TAGS = {
LIST_EMPTY: 0,
STREAM_END: 2,
DICTIONARY_0: 236,
DICTIONARY_1: 237,
DICTIONARY_2: 238,
DICTIONARY_3: 239,
LIST_8: 248,
LIST_16: 249,
JID_PAIR: 250,
HEX_8: 251,
BINARY_8: 252,
BINARY_20: 253,
BINARY_32: 254,
NIBBLE_8: 255,
SINGLE_BYTE_MAX: 256,
PACKED_MAX: 254,
AD_JID: 247,
}
export const DOUBLE_BYTE_TOKENS = []
export const SINGLE_BYTE_TOKENS = [
null,
null,
null,
'200',
'400',
'404',
'500',
'501',
'502',
'action',
'add',
'after',
'archive',
'author',
'available',
'battery',
'before',
'body',
'broadcast',
'chat',
'clear',
'code',
'composing',
'contacts',
'count',
'create',
'debug',
'delete',
'demote',
'duplicate',
'encoding',
'error',
'false',
'filehash',
'from',
'g.us',
'group',
'groups_v2',
'height',
'id',
'image',
'in',
'index',
'invis',
'item',
'jid',
'kind',
'last',
'leave',
'live',
'log',
'media',
'message',
'mimetype',
'missing',
'modify',
'name',
'notification',
'notify',
'out',
'owner',
'participant',
'paused',
'picture',
'played',
'presence',
'preview',
'promote',
'query',
'raw',
'read',
'receipt',
'received',
'recipient',
'recording',
'relay',
'remove',
'response',
'resume',
'retry',
's.whatsapp.net',
'seconds',
'set',
'size',
'status',
'subject',
'subscribe',
't',
'text',
'to',
'true',
'type',
'unarchive',
'unavailable',
'url',
'user',
'value',
'web',
'width',
'mute',
'read_only',
'admin',
'creator',
'short',
'update',
'powersave',
'checksum',
'epoch',
'block',
'previous',
'409',
'replaced',
'reason',
'spam',
'modify_tag',
'message_info',
'delivery',
'emoji',
'title',
'description',
'canonical-url',
'matched-text',
'star',
'unstar',
'media_key',
'filename',
'identity',
'unread',
'page',
'page_count',
'search',
'media_message',
'security',
'call_log',
'profile',
'ciphertext',
'invite',
'gif',
'vcard',
'frequent',
'privacy',
'blacklist',
'whitelist',
'verify',
'location',
'document',
'elapsed',
'revoke_invite',
'expiration',
'unsubscribe',
'disable',
'vname',
'old_jid',
'new_jid',
'announcement',
'locked',
'prop',
'label',
'color',
'call',
'offer',
'call-id',
'quick_reply',
'sticker',
'pay_t',
'accept',
'reject',
'sticker_pack',
'invalid',
'canceled',
'missed',
'connected',
'result',
'audio',
'video',
'recent',
]
export const TOKEN_MAP: { [token: string]: { dict?: number, index: number } } = { }
for(let i = 0;i < SINGLE_BYTE_TOKENS.length;i++) {
TOKEN_MAP[SINGLE_BYTE_TOKENS[i]] = { index: i }
}

View File

@@ -1,376 +1,13 @@
import { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } from '../constants'
import { decodeDecompressedBinaryNode } from '../decode'
import { encodeBinaryNode } from '../encode'
import type { BinaryNode } from '../types'
import * as constants from './constants'
export const isLegacyBinaryNode = (buffer: Buffer) => {
switch (buffer[0]) {
case TAGS.LIST_EMPTY:
case TAGS.LIST_8:
case TAGS.LIST_16:
return true
default:
return false
}
export const encodeBinaryNodeLegacy = (node: BinaryNode) => {
return encodeBinaryNode(node, constants, [])
}
function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
const checkEOS = (length: number) => {
if(indexRef.index + length > buffer.length) {
throw new Error('end of stream')
}
}
const next = () => {
const value = buffer[indexRef.index]
indexRef.index += 1
return value
}
const readByte = () => {
checkEOS(1)
return next()
}
const readStringFromChars = (length: number) => {
checkEOS(length)
const value = buffer.slice(indexRef.index, indexRef.index + length)
indexRef.index += length
return value.toString('utf-8')
}
const readBytes = (n: number) => {
checkEOS(n)
const value = buffer.slice(indexRef.index, indexRef.index + n)
indexRef.index += n
return value
}
const readInt = (n: number, littleEndian = false) => {
checkEOS(n)
let val = 0
for(let i = 0; i < n; i++) {
const shift = littleEndian ? i : n - 1 - i
val |= next() << (shift * 8)
}
return val
}
const readInt20 = () => {
checkEOS(3)
return ((next() & 15) << 16) + (next() << 8) + next()
}
const unpackHex = (value: number) => {
if(value >= 0 && value < 16) {
return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10
}
throw new Error('invalid hex: ' + value)
}
const unpackNibble = (value: number) => {
if(value >= 0 && value <= 9) {
return '0'.charCodeAt(0) + value
}
switch (value) {
case 10:
return '-'.charCodeAt(0)
case 11:
return '.'.charCodeAt(0)
case 15:
return '\0'.charCodeAt(0)
default:
throw new Error('invalid nibble: ' + value)
}
}
const unpackByte = (tag: number, value: number) => {
if(tag === TAGS.NIBBLE_8) {
return unpackNibble(value)
} else if(tag === TAGS.HEX_8) {
return unpackHex(value)
} else {
throw new Error('unknown tag: ' + tag)
}
}
const readPacked8 = (tag: number) => {
const startByte = readByte()
let value = ''
for(let i = 0; i < (startByte & 127); i++) {
const curByte = readByte()
value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4))
value += String.fromCharCode(unpackByte(tag, curByte & 0x0f))
}
if(startByte >> 7 !== 0) {
value = value.slice(0, -1)
}
return value
}
const isListTag = (tag: number) => {
return tag === TAGS.LIST_EMPTY || tag === TAGS.LIST_8 || tag === TAGS.LIST_16
}
const readListSize = (tag: number) => {
switch (tag) {
case TAGS.LIST_EMPTY:
return 0
case TAGS.LIST_8:
return readByte()
case TAGS.LIST_16:
return readInt(2)
default:
throw new Error('invalid tag for list size: ' + tag)
}
}
const getToken = (index: number) => {
if(index < 3 || index >= SINGLE_BYTE_TOKENS.length) {
throw new Error('invalid token index: ' + index)
}
return SINGLE_BYTE_TOKENS[index]
}
const readString = (tag: number) => {
if(tag >= 3 && tag <= 235) {
const token = getToken(tag)
return token// === 's.whatsapp.net' ? 'c.us' : token
}
switch (tag) {
case TAGS.DICTIONARY_0:
case TAGS.DICTIONARY_1:
case TAGS.DICTIONARY_2:
case TAGS.DICTIONARY_3:
return getTokenDouble(tag - TAGS.DICTIONARY_0, readByte())
case TAGS.LIST_EMPTY:
return null
case TAGS.BINARY_8:
return readStringFromChars(readByte())
case TAGS.BINARY_20:
return readStringFromChars(readInt20())
case TAGS.BINARY_32:
return readStringFromChars(readInt(4))
case TAGS.JID_PAIR:
const i = readString(readByte())
const j = readString(readByte())
if(typeof i === 'string' && j) {
return i + '@' + j
}
throw new Error('invalid jid pair: ' + i + ', ' + j)
case TAGS.HEX_8:
case TAGS.NIBBLE_8:
return readPacked8(tag)
default:
throw new Error('invalid string with tag: ' + tag)
}
}
const readList = (tag: number) => (
[...new Array(readListSize(tag))].map(() => decode(buffer, indexRef))
)
const getTokenDouble = (index1: number, index2: number) => {
const n = 256 * index1 + index2
if(n < 0 || n > DOUBLE_BYTE_TOKENS.length) {
throw new Error('invalid double token index: ' + n)
}
return DOUBLE_BYTE_TOKENS[n]
}
const listSize = readListSize(readByte())
const descrTag = readByte()
if(descrTag === TAGS.STREAM_END) {
throw new Error('unexpected stream end')
}
const header = readString(descrTag)
const attrs: BinaryNode['attrs'] = { }
let data: BinaryNode['content']
if(listSize === 0 || !header) {
throw new Error('invalid node')
}
// read the attributes in
const attributesLength = (listSize - 1) >> 1
for(let i = 0; i < attributesLength; i++) {
const key = readString(readByte())
const b = readByte()
attrs[key] = readString(b)
}
if(listSize % 2 === 0) {
const tag = readByte()
if(isListTag(tag)) {
data = readList(tag)
} else {
let decoded: Buffer | string
switch (tag) {
case TAGS.BINARY_8:
decoded = readBytes(readByte())
break
case TAGS.BINARY_20:
decoded = readBytes(readInt20())
break
case TAGS.BINARY_32:
decoded = readBytes(readInt(4))
break
default:
decoded = readString(tag)
break
}
data = decoded
}
}
return {
tag: header,
attrs,
content: data
}
export const decodeBinaryNodeLegacy = (data: Buffer, indexRef: { index: number }) => {
return decodeDecompressedBinaryNode(data, constants, indexRef)
}
const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
const pushByte = (value: number) => buffer.push(value & 0xff)
const pushInt = (value: number, n: number, littleEndian = false) => {
for(let i = 0; i < n; i++) {
const curShift = littleEndian ? i : n - 1 - i
buffer.push((value >> (curShift * 8)) & 0xff)
}
}
const pushBytes = (bytes: Uint8Array | Buffer | number[]) => (
bytes.forEach (b => buffer.push(b))
)
const pushInt20 = (value: number) => (
pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff])
)
const writeByteLength = (length: number) => {
if(length >= 4294967296) {
throw new Error('string too large to encode: ' + length)
}
if(length >= 1 << 20) {
pushByte(TAGS.BINARY_32)
pushInt(length, 4) // 32 bit integer
} else if(length >= 256) {
pushByte(TAGS.BINARY_20)
pushInt20(length)
} else {
pushByte(TAGS.BINARY_8)
pushByte(length)
}
}
const writeStringRaw = (str: string) => {
const bytes = Buffer.from (str, 'utf-8')
writeByteLength(bytes.length)
pushBytes(bytes)
}
const writeToken = (token: number) => {
if(token < 245) {
pushByte(token)
} else if(token <= 500) {
throw new Error('invalid token')
}
}
const writeString = (token: string, i?: boolean) => {
if(token === 'c.us') {
token = 's.whatsapp.net'
}
const tokenIndex = SINGLE_BYTE_TOKENS.indexOf(token)
if(!i && token === 's.whatsapp.net') {
writeToken(tokenIndex)
} else if(tokenIndex >= 0) {
if(tokenIndex < TAGS.SINGLE_BYTE_MAX) {
writeToken(tokenIndex)
} else {
const overflow = tokenIndex - TAGS.SINGLE_BYTE_MAX
const dictionaryIndex = overflow >> 8
if(dictionaryIndex < 0 || dictionaryIndex > 3) {
throw new Error('double byte dict token out of range: ' + token + ', ' + tokenIndex)
}
writeToken(TAGS.DICTIONARY_0 + dictionaryIndex)
writeToken(overflow % 256)
}
} else if(token) {
const jidSepIndex = token.indexOf('@')
if(jidSepIndex <= 0) {
writeStringRaw(token)
} else {
writeJid(token.slice(0, jidSepIndex), token.slice(jidSepIndex + 1, token.length))
}
}
}
const writeJid = (left: string, right: string) => {
pushByte(TAGS.JID_PAIR)
left && left.length > 0 ? writeString(left) : writeToken(TAGS.LIST_EMPTY)
writeString(right)
}
const writeListStart = (listSize: number) => {
if(listSize === 0) {
pushByte(TAGS.LIST_EMPTY)
} else if(listSize < 256) {
pushBytes([TAGS.LIST_8, listSize])
} else {
pushBytes([TAGS.LIST_16, listSize])
}
}
const validAttributes = Object.keys(attrs).filter(k => (
typeof attrs[k] !== 'undefined' && attrs[k] !== null
))
writeListStart(2 * validAttributes.length + 1 + (typeof content !== 'undefined' && content !== null ? 1 : 0))
writeString(tag)
validAttributes.forEach((key) => {
if(typeof attrs[key] === 'string') {
writeString(key)
writeString(attrs[key])
}
})
if(typeof content === 'string') {
writeString(content, true)
} else if(Buffer.isBuffer(content)) {
writeByteLength(content.length)
pushBytes(content)
} else if(Array.isArray(content)) {
writeListStart(content.length)
for(const item of content) {
if(item) {
encode(item, buffer)
}
}
} else if(typeof content === 'undefined' || content === null) {
} else {
throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`)
}
return Buffer.from(buffer)
}
export const encodeBinaryNodeLegacy = encode
export const decodeBinaryNodeLegacy = decode

View File

@@ -29,7 +29,8 @@ export const SINGLE_BYTE_TOKENS = [
'', 'xmlstreamstart', 'xmlstreamend', 's.whatsapp.net', 'type', 'participant', 'from', 'receipt', 'id', 'broadcast', 'status', 'message', 'notification', 'notify', 'to', 'jid', 'user', 'class', 'offline', 'g.us', 'result', 'mediatype', 'enc', 'skmsg', 'off_cnt', 'xmlns', 'presence', 'participants', 'ack', 't', 'iq', 'device_hash', 'read', 'value', 'media', 'picture', 'chatstate', 'unavailable', 'text', 'urn:xmpp:whatsapp:push', 'devices', 'verified_name', 'contact', 'composing', 'edge_routing', 'routing_info', 'item', 'image', 'verified_level', 'get', 'fallback_hostname', '2', 'media_conn', '1', 'v', 'handshake', 'fallback_class', 'count', 'config', 'offline_preview', 'download_buckets', 'w:profile:picture', 'set', 'creation', 'location', 'fallback_ip4', 'msg', 'urn:xmpp:ping', 'fallback_ip6', 'call-creator', 'relaylatency', 'success', 'subscribe', 'video', 'business_hours_config', 'platform', 'hostname', 'version', 'unknown', '0', 'ping', 'hash', 'edit', 'subject', 'max_buckets', 'download', 'delivery', 'props', 'sticker', 'name', 'last', 'contacts', 'business', 'primary', 'preview', 'w:p', 'pkmsg', 'call-id', 'retry', 'prop', 'call', 'auth_ttl', 'available', 'relay_id', 'last_id', 'day_of_week', 'w', 'host', 'seen', 'bits', 'list', 'atn', 'upload', 'is_new', 'w:stats', 'key', 'paused', 'specific_hours', 'multicast', 'stream:error', 'mmg.whatsapp.net', 'code', 'deny', 'played', 'profile', 'fna', 'device-list', 'close_time', 'latency', 'gcm', 'pop', 'audio', '26', 'w:web', 'open_time', 'error', 'auth', 'ip4', 'update', 'profile_options', 'config_value', 'category', 'catalog_not_created', '00', 'config_code', 'mode', 'catalog_status', 'ip6', 'blocklist', 'registration', '7', 'web', 'fail', 'w:m', 'cart_enabled', 'ttl', 'gif', '300', 'device_orientation', 'identity', 'query', '401', 'media-gig2-1.cdn.whatsapp.net', 'in', '3', 'te2', 'add', 'fallback', 'categories', 'ptt', 'encrypt', 'notice', 'thumbnail-document', 'item-not-found', '12', 'thumbnail-image', 'stage', 'thumbnail-link', 'usync', 'out', 'thumbnail-video', '8', '01', 'context', 'sidelist', 'thumbnail-gif', 'terminate', 'not-authorized', 'orientation', 'dhash', 'capability', 'side_list', 'md-app-state', 'description', 'serial', 'readreceipts', 'te', 'business_hours', 'md-msg-hist', 'tag', 'attribute_padding', 'document', 'open_24h', 'delete', 'expiration', 'active', 'prev_v_id', 'true', 'passive', 'index', '4', 'conflict', 'remove', 'w:gp2', 'config_expo_key', 'screen_height', 'replaced', '02', 'screen_width', 'uploadfieldstat', '2:47DEQpj8', 'media-bog1-1.cdn.whatsapp.net', 'encopt', 'url', 'catalog_exists', 'keygen', 'rate', 'offer', 'opus', 'media-mia3-1.cdn.whatsapp.net', 'privacy', 'media-mia3-2.cdn.whatsapp.net', 'signature', 'preaccept', 'token_id', 'media-eze1-1.cdn.whatsapp.net'
]
const TOKEN_MAP: { [token: string]: { dict?: number, index: number } } = { }
export const TOKEN_MAP: { [token: string]: { dict?: number, index: number } } = { }
for(let i = 0;i < SINGLE_BYTE_TOKENS.length;i++) {
TOKEN_MAP[SINGLE_BYTE_TOKENS[i]] = { index: i }
}
@@ -38,6 +39,4 @@ for(let i = 0;i < DOUBLE_BYTE_TOKENS.length;i++) {
for(let j = 0;j < DOUBLE_BYTE_TOKENS[i].length;j++) {
TOKEN_MAP[DOUBLE_BYTE_TOKENS[i][j]] = { dict: i, index: j }
}
}
export { TOKEN_MAP }
}

View File

@@ -1,7 +1,7 @@
import { inflateSync } from 'zlib'
import { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } from './constants'
import * as constants from './constants'
import { jidEncode } from './jid-utils'
import type { BinaryNode } from './types'
import type { BinaryNode, BinaryNodeCodingOptions } from './types'
export const decompressingIfRequired = (buffer: Buffer) => {
if(2 & buffer.readUInt8()) {
@@ -13,7 +13,12 @@ export const decompressingIfRequired = (buffer: Buffer) => {
return buffer
}
export const decodeDecompressedBinaryNode = (buffer: Buffer, indexRef: { index: number } = { index: 0 }): BinaryNode => {
export const decodeDecompressedBinaryNode = (
buffer: Buffer,
opts: Pick<BinaryNodeCodingOptions, 'DOUBLE_BYTE_TOKENS' | 'SINGLE_BYTE_TOKENS' | 'TAGS'>,
indexRef: { index: number } = { index: 0 }
): BinaryNode => {
const { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } = opts
const checkEOS = (length: number) => {
if(indexRef.index + length > buffer.length) {
@@ -181,7 +186,7 @@ export const decodeDecompressedBinaryNode = (buffer: Buffer, indexRef: { index:
const items: BinaryNode[] = []
const size = readListSize(tag)
for(let i = 0;i < size;i++) {
items.push(decodeDecompressedBinaryNode(buffer, indexRef))
items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef))
}
return items
@@ -256,5 +261,5 @@ export const decodeDecompressedBinaryNode = (buffer: Buffer, indexRef: { index:
export const decodeBinaryNode = (buff: Buffer): BinaryNode => {
const decompBuff = decompressingIfRequired(buff)
return decodeDecompressedBinaryNode(decompBuff)
return decodeDecompressedBinaryNode(decompBuff, constants)
}

View File

@@ -1,9 +1,14 @@
import { TAGS, TOKEN_MAP } from './constants'
import * as constants from './constants'
import { jidDecode } from './jid-utils'
import type { BinaryNode } from './types'
import type { BinaryNode, BinaryNodeCodingOptions } from './types'
export const encodeBinaryNode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = [0]) => {
export const encodeBinaryNode = (
{ tag, attrs, content }: BinaryNode,
opts: Pick<BinaryNodeCodingOptions, 'TAGS' | 'TOKEN_MAP'> = constants,
buffer: number[] = []
) => {
const { TAGS, TOKEN_MAP } = opts
const pushByte = (value: number) => buffer.push(value & 0xff)
@@ -216,7 +221,7 @@ export const encodeBinaryNode = ({ tag, attrs, content }: BinaryNode, buffer: nu
writeListStart(content.length)
for(const item of content) {
if(item) {
encodeBinaryNode(item, buffer)
encodeBinaryNode(item, opts, buffer)
}
}
} else if(typeof content === 'undefined' || content === null) {

View File

@@ -1,3 +1,4 @@
import * as constants from './constants'
/**
* the binary node WA uses internally for communication
*
@@ -11,4 +12,6 @@ export type BinaryNode = {
content?: BinaryNode[] | string | Uint8Array
}
export type BinaryNodeAttributes = BinaryNode['attrs']
export type BinaryNodeData = BinaryNode['content']
export type BinaryNodeData = BinaryNode['content']
export type BinaryNodeCodingOptions = typeof constants