refactor: use modified legacy bin XML parser instead of "WABinary"

This commit is contained in:
Adhiraj Singh
2022-04-13 16:23:28 +05:30
parent 6708775cb5
commit b4fd010b36
14 changed files with 604 additions and 1343 deletions

View File

@@ -62,7 +62,7 @@ export const makeSocket = ({
const sendPromise = promisify<void>(ws.send)
/** send a raw buffer */
const sendRawMessage = async(data: Buffer | Uint8Array) => {
const sendRawMessage = async(data: Uint8Array | Buffer) => {
if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
}

View File

@@ -6,7 +6,6 @@ import { Logger } from 'pino'
import { proto } from '../../WAProto'
import { version as baileysVersion } from '../Defaults/baileys-version.json'
import { CommonBaileysEventEmitter, ConnectionState, DisconnectReason, WAVersion } from '../Types'
import { Binary } from '../WABinary'
const PLATFORM_MAP = {
'aix': 'AIX',
@@ -41,16 +40,14 @@ export const BufferJSON = {
}
}
export const writeRandomPadMax16 = (e: Binary) => {
function r(e: Binary, t: number) {
for(var r = 0; r < t; r++) {
e.writeUint8(t)
}
export const writeRandomPadMax16 = (msg: Uint8Array) => {
const pad = randomBytes(1)
pad[0] &= 0xf
if(!pad[0]) {
pad[0] = 0xf
}
var t = randomBytes(1)
r(e, 1 + (15 & t[0]))
return e
return Buffer.concat([msg, Buffer.alloc(pad[0], pad[0])])
}
export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
@@ -68,10 +65,8 @@ export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
}
export const encodeWAMessage = (message: proto.IMessage) => (
Buffer.from(
writeRandomPadMax16(
new Binary(proto.Message.encode(message).finish())
).readByteArray()
writeRandomPadMax16(
proto.Message.encode(message).finish()
)
)

View File

@@ -3,7 +3,6 @@ import { createCipheriv, createDecipheriv } from 'crypto'
import { proto } from '../../WAProto'
import { NOISE_MODE, NOISE_WA_HEADER } from '../Defaults'
import { KeyPair } from '../Types'
import { Binary } from '../WABinary'
import { BinaryNode, decodeBinaryNode } from '../WABinary'
import { Curve, hkdf, sha256 } from './crypto'
@@ -94,8 +93,7 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
let isFinished = false
let sentIntro = false
const outBinary = new Binary()
const inBinary = new Binary()
let inBytes = Buffer.alloc(0)
authenticate(NOISE_WA_HEADER)
authenticate(publicKey)
@@ -133,47 +131,43 @@ export const makeNoiseHandler = ({ public: publicKey, private: privateKey }: Key
}
const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length
outBinary.ensureAdditionalCapacity(introSize + 3 + data.byteLength)
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
if(!sentIntro) {
outBinary.writeByteArray(NOISE_WA_HEADER)
frame.set(NOISE_WA_HEADER)
sentIntro = true
}
outBinary.writeUint8(data.byteLength >> 16)
outBinary.writeUint16(65535 & data.byteLength)
outBinary.write(data)
frame.writeUInt8(data.byteLength >> 16, introSize)
frame.writeUInt16BE(65535 & data.byteLength, introSize + 1)
frame.set(data, introSize + 3)
const bytes = outBinary.readByteArray()
return bytes as Uint8Array
return frame
},
decodeFrame: (newData: Buffer | Uint8Array, onFrame: (buff: Uint8Array | BinaryNode) => void) => {
// the binary protocol uses its own framing mechanism
// on top of the WS frames
// so we get this data and separate out the frames
const getBytesSize = () => {
return (inBinary.readUint8() << 16) | inBinary.readUint16()
if(inBytes.length >= 3) {
return (inBytes.readUInt8() << 16) | inBytes.readUInt16BE(1)
}
}
const peekSize = () => {
return !(inBinary.size() < 3) && getBytesSize() <= inBinary.size()
}
inBytes = Buffer.concat([ inBytes, newData ])
let size = getBytesSize()
while(size && inBytes.length >= size + 3) {
let frame: Uint8Array | BinaryNode = inBytes.slice(3, size + 3)
inBytes = inBytes.slice(size + 3)
inBinary.writeByteArray(newData)
while(inBinary.peek(peekSize)) {
const bytes = getBytesSize()
let frame: Uint8Array | BinaryNode = inBinary.readByteArray(bytes)
if(isFinished) {
const result = decrypt(frame as Uint8Array)
const unpacked = new Binary(result).decompressed()
frame = decodeBinaryNode(unpacked)
frame = decodeBinaryNode(result)
}
onFrame(frame)
size = getBytesSize()
}
inBinary.peek(peekSize)
}
}
}

View File

@@ -1,198 +0,0 @@
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,
}
export const DoubleByteTokens = []
export const SingleByteTokens = [
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',
]

View File

@@ -1,12 +1,12 @@
import { BinaryNode } from '../types'
import { DoubleByteTokens, SingleByteTokens, Tags } from './constants'
import { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } from '../constants'
import type { BinaryNode } from '../types'
export const isLegacyBinaryNode = (buffer: Buffer) => {
switch (buffer[0]) {
case Tags.LIST_EMPTY:
case Tags.LIST_8:
case Tags.LIST_16:
case TAGS.LIST_EMPTY:
case TAGS.LIST_8:
case TAGS.LIST_16:
return true
default:
return false
@@ -89,9 +89,9 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
}
const unpackByte = (tag: number, value: number) => {
if(tag === Tags.NIBBLE_8) {
if(tag === TAGS.NIBBLE_8) {
return unpackNibble(value)
} else if(tag === Tags.HEX_8) {
} else if(tag === TAGS.HEX_8) {
return unpackHex(value)
} else {
throw new Error('unknown tag: ' + tag)
@@ -116,16 +116,16 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
}
const isListTag = (tag: number) => {
return tag === Tags.LIST_EMPTY || tag === Tags.LIST_8 || tag === Tags.LIST_16
return tag === TAGS.LIST_EMPTY || tag === TAGS.LIST_8 || tag === TAGS.LIST_16
}
const readListSize = (tag: number) => {
switch (tag) {
case Tags.LIST_EMPTY:
case TAGS.LIST_EMPTY:
return 0
case Tags.LIST_8:
case TAGS.LIST_8:
return readByte()
case Tags.LIST_16:
case TAGS.LIST_16:
return readInt(2)
default:
throw new Error('invalid tag for list size: ' + tag)
@@ -133,11 +133,11 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
}
const getToken = (index: number) => {
if(index < 3 || index >= SingleByteTokens.length) {
if(index < 3 || index >= SINGLE_BYTE_TOKENS.length) {
throw new Error('invalid token index: ' + index)
}
return SingleByteTokens[index]
return SINGLE_BYTE_TOKENS[index]
}
const readString = (tag: number) => {
@@ -147,20 +147,20 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
}
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:
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:
case TAGS.BINARY_8:
return readStringFromChars(readByte())
case Tags.BINARY_20:
case TAGS.BINARY_20:
return readStringFromChars(readInt20())
case Tags.BINARY_32:
case TAGS.BINARY_32:
return readStringFromChars(readInt(4))
case Tags.JID_PAIR:
case TAGS.JID_PAIR:
const i = readString(readByte())
const j = readString(readByte())
if(typeof i === 'string' && j) {
@@ -168,8 +168,8 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
}
throw new Error('invalid jid pair: ' + i + ', ' + j)
case Tags.HEX_8:
case Tags.NIBBLE_8:
case TAGS.HEX_8:
case TAGS.NIBBLE_8:
return readPacked8(tag)
default:
throw new Error('invalid string with tag: ' + tag)
@@ -181,16 +181,16 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
)
const getTokenDouble = (index1: number, index2: number) => {
const n = 256 * index1 + index2
if(n < 0 || n > DoubleByteTokens.length) {
if(n < 0 || n > DOUBLE_BYTE_TOKENS.length) {
throw new Error('invalid double token index: ' + n)
}
return DoubleByteTokens[n]
return DOUBLE_BYTE_TOKENS[n]
}
const listSize = readListSize(readByte())
const descrTag = readByte()
if(descrTag === Tags.STREAM_END) {
if(descrTag === TAGS.STREAM_END) {
throw new Error('unexpected stream end')
}
@@ -217,13 +217,13 @@ function decode(buffer: Buffer, indexRef: { index: number }): BinaryNode {
} else {
let decoded: Buffer | string
switch (tag) {
case Tags.BINARY_8:
case TAGS.BINARY_8:
decoded = readBytes(readByte())
break
case Tags.BINARY_20:
case TAGS.BINARY_20:
decoded = readBytes(readInt20())
break
case Tags.BINARY_32:
case TAGS.BINARY_32:
decoded = readBytes(readInt(4))
break
default:
@@ -265,13 +265,13 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
}
if(length >= 1 << 20) {
pushByte(Tags.BINARY_32)
pushByte(TAGS.BINARY_32)
pushInt(length, 4) // 32 bit integer
} else if(length >= 256) {
pushByte(Tags.BINARY_20)
pushByte(TAGS.BINARY_20)
pushInt20(length)
} else {
pushByte(Tags.BINARY_8)
pushByte(TAGS.BINARY_8)
pushByte(length)
}
}
@@ -295,20 +295,20 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
token = 's.whatsapp.net'
}
const tokenIndex = SingleByteTokens.indexOf(token)
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) {
if(tokenIndex < TAGS.SINGLE_BYTE_MAX) {
writeToken(tokenIndex)
} else {
const overflow = tokenIndex - Tags.SINGLE_BYTE_MAX
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(TAGS.DICTIONARY_0 + dictionaryIndex)
writeToken(overflow % 256)
}
} else if(token) {
@@ -322,18 +322,18 @@ const encode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = []) => {
}
const writeJid = (left: string, right: string) => {
pushByte(Tags.JID_PAIR)
left && left.length > 0 ? writeString(left) : writeToken(Tags.LIST_EMPTY)
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)
pushByte(TAGS.LIST_EMPTY)
} else if(listSize < 256) {
pushBytes([Tags.LIST_8, listSize])
pushBytes([TAGS.LIST_8, listSize])
} else {
pushBytes([Tags.LIST_16, listSize])
pushBytes([TAGS.LIST_16, listSize])
}
}

43
src/WABinary/constants.ts Normal file

File diff suppressed because one or more lines are too long

260
src/WABinary/decode.ts Normal file
View File

@@ -0,0 +1,260 @@
import { inflateSync } from 'zlib'
import { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } from './constants'
import { jidEncode } from './jid-utils'
import type { BinaryNode } from './types'
export const decompressingIfRequired = (buffer: Buffer) => {
if(2 & buffer.readUInt8()) {
buffer = inflateSync(buffer.slice(1))
} else { // nodes with no compression have a 0x00 prefix, we remove that
buffer = buffer.slice(1)
}
return buffer
}
export const decodeDecompressedBinaryNode = (buffer: Buffer, indexRef: { index: number } = { index: 0 }): 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 readBytes = (n: number) => {
checkEOS(n)
const value = buffer.slice(indexRef.index, indexRef.index + n)
indexRef.index += n
return value
}
const readStringFromChars = (length: number) => {
return readBytes(length).toString('utf-8')
}
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 readJidPair = () => {
const i = readString(readByte())
const j = readString(readByte())
if(j) {
return (i || '') + '@' + j
}
throw new Error('invalid jid pair: ' + i + ', ' + j)
}
const readAdJid = () => {
const agent = readByte()
const device = readByte()
const user = readString(readByte())
return jidEncode(user, 's.whatsapp.net', device, agent)
}
const readString = (tag: number): string => {
if(tag >= 1 && tag < SINGLE_BYTE_TOKENS.length) {
return SINGLE_BYTE_TOKENS[tag]
}
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:
return readJidPair()
case TAGS.AD_JID:
return readAdJid()
case TAGS.HEX_8:
case TAGS.NIBBLE_8:
return readPacked8(tag)
default:
throw new Error('invalid string with tag: ' + tag)
}
}
const readList = (tag: number) => {
const items: BinaryNode[] = []
const size = readListSize(tag)
for(let i = 0;i < size;i++) {
items.push(decodeDecompressedBinaryNode(buffer, indexRef))
}
return items
}
const getTokenDouble = (index1: number, index2: number) => {
const dict = DOUBLE_BYTE_TOKENS[index1]
if(!dict) {
throw new Error(`Invalid double token dict (${index1})`)
}
const value = dict[index2]
if(typeof value === 'undefined') {
throw new Error(`Invalid double token (${index2})`)
}
return value
}
const listSize = readListSize(readByte())
const header = readString(readByte())
if(!listSize || !header.length) {
throw new Error('invalid node')
}
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 value = readString(readByte())
attrs[key] = value
}
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 decodeBinaryNode = (buff: Buffer): BinaryNode => {
const decompBuff = decompressingIfRequired(buff)
return decodeDecompressedBinaryNode(decompBuff)
}

229
src/WABinary/encode.ts Normal file
View File

@@ -0,0 +1,229 @@
import { TAGS, TOKEN_MAP } from './constants'
import { jidDecode } from './jid-utils'
import type { BinaryNode } from './types'
export const encodeBinaryNode = ({ tag, attrs, content }: BinaryNode, buffer: number[] = [0]) => {
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 writeJid = ({ agent, device, user, server }: ReturnType<typeof jidDecode>) => {
if(typeof agent !== 'undefined' || typeof device !== 'undefined') {
pushByte(TAGS.AD_JID)
pushByte(agent)
pushByte(device)
writeString(user)
} else {
pushByte(TAGS.JID_PAIR)
if(user.length) {
writeString(user)
} else {
pushByte(TAGS.LIST_EMPTY)
}
writeString(server)
}
}
const packNibble = (char: string) => {
switch (char) {
case '-':
return 10
case '.':
return 11
case '\0':
return 15
default:
if(char >= '0' && char <= '9') {
return char.charCodeAt(0) - '0'.charCodeAt(0)
}
throw new Error(`invalid byte for nibble "${char}"`)
}
}
const packHex = (char: string) => {
if(char >= '0' && char <= '9') {
return char.charCodeAt(0) - '0'.charCodeAt(0)
}
if(char >= 'A' && char <= 'F') {
return 10 + char.charCodeAt(0) - 'A'.charCodeAt(0)
}
if(char >= 'a' && char <= 'f') {
return 10 + char.charCodeAt(0) - 'a'.charCodeAt(0)
}
if(char === '\0') {
return 15
}
throw new Error(`Invalid hex char "${char}"`)
}
const writePackedBytes = (str: string, type: 'nibble' | 'hex') => {
if(str.length > TAGS.PACKED_MAX) {
throw new Error('Too many bytes to pack')
}
pushByte(type === 'nibble' ? TAGS.NIBBLE_8 : TAGS.HEX_8)
let roundedLength = Math.ceil(str.length / 2.0)
if(str.length % 2 !== 0) {
roundedLength |= 128
}
pushByte(roundedLength)
const packFunction = type === 'nibble' ? packNibble : packHex
const packBytePair = (v1: string, v2: string) => {
const result = (packFunction(v1) << 4) | packFunction(v2)
return result
}
const strLengthHalf = Math.floor(str.length / 2)
for(let i = 0; i < strLengthHalf;i++) {
pushByte(packBytePair(str[2 * i], str[2 * i + 1]))
}
if(str.length % 2 !== 0) {
pushByte(packBytePair(str[str.length - 1], '\x00'))
}
}
const isNibble = (str: string) => {
if(str.length > TAGS.PACKED_MAX) {
return false
}
for(let i = 0;i < str.length;i++) {
const char = str[i]
const isInNibbleRange = char >= '0' && char <= '9'
if(!isInNibbleRange && char !== '-' && char !== '.') {
return false
}
}
return true
}
const isHex = (str: string) => {
if(str.length > TAGS.PACKED_MAX) {
return false
}
for(let i = 0;i < str.length;i++) {
const char = str[i]
const isInNibbleRange = char >= '0' && char <= '9'
if(!isInNibbleRange && !(char >= 'A' && char <= 'F') && !(char >= 'a' && char <= 'f')) {
return false
}
}
return true
}
const writeString = (str: string) => {
// console.log('before write of ', str, ' ', Buffer.from(buffer).toString('hex'))
const tokenIndex = TOKEN_MAP[str]
if(tokenIndex) {
if(typeof tokenIndex.dict === 'number') {
pushByte(TAGS.DICTIONARY_0 + tokenIndex.dict)
}
pushByte(tokenIndex.index)
} else if(isNibble(str)) {
writePackedBytes(str, 'nibble')
} else if(isHex(str)) {
writePackedBytes(str, 'hex')
} else if(str) {
const decodedJid = jidDecode(str)
if(decodedJid) {
writeJid(decodedJid)
} else {
writeStringRaw(str)
}
}
}
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)
for(const key of validAttributes) {
if(typeof attrs[key] === 'string') {
writeString(key)
writeString(attrs[key])
}
}
if(typeof content === 'string') {
writeString(content)
} else if(Buffer.isBuffer(content) || content instanceof Uint8Array) {
writeByteLength(content.length)
pushBytes(content)
} else if(Array.isArray(content)) {
writeListStart(content.length)
for(const item of content) {
if(item) {
encodeBinaryNode(item, buffer)
}
}
} else if(typeof content === 'undefined' || content === null) {
// do nothing
} else {
throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`)
}
return Buffer.from(buffer)
}

View File

@@ -1,326 +1,6 @@
import { DICTIONARIES_MAP, SINGLE_BYTE_TOKEN, SINGLE_BYTE_TOKEN_MAP, DICTIONARIES } from '../../WABinary/Constants';
import { jidDecode, jidEncode } from './jid-utils';
import { Binary, numUtf8Bytes } from '../../WABinary/Binary';
import { Boom } from '@hapi/boom';
import { proto } from '../../WAProto';
import { BinaryNode } from './types';
const LIST1 = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.', '<27>', '<27>', '<27>', '<27>'];
const LIST2 = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
function k(data: Binary, uint: number) {
let arr = [];
for (let a = 0; a < uint; a++) {
arr.push(decodeBinaryNode(data));
}
return arr;
}
function x(data: Binary, t, r, a) {
const arr = new Array(2 * a - r);
for (let n = 0; n < arr.length - 1; n += 2) {
var s = data.readUint8();
(arr[n] = t[s >>> 4]), (arr[n + 1] = t[15 & s]);
}
if (r) {
arr[arr.length - 1] = t[data.readUint8() >>> 4];
}
return arr.join('');
}
function D(e, t, r) {
var a = e.length % 2 == 1;
r.writeUint8(t);
var i = Math.ceil(e.length / 2);
a && (i |= 128), r.writeUint8(i);
for (var n = 0, s = 0; s < e.length; s++) {
var o = e.charCodeAt(s),
l = null;
if ((48 <= o && o <= 57 ? (l = o - 48) : 255 === t ? (45 === o ? (l = 10) : 46 === o && (l = 11)) : 251 === t && 65 <= o && o <= 70 && (l = o - 55), null == l))
throw new Error(`Cannot nibble encode ${o}`);
s % 2 == 0 ? ((n = l << 4), s === e.length - 1 && ((n |= 15), r.writeUint8(n))) : ((n |= l), r.writeUint8(n));
}
}
function N(e, t) {
if (e < 256) t.writeUint8(252), t.writeUint8(e);
else if (e < 1048576) t.writeUint8(253), t.writeUint8((e >>> 16) & 255), t.writeUint8((e >>> 8) & 255), t.writeUint8(255 & e);
else {
if (!(e < 4294967296)) throw new Error(`Binary with length ${e} is too big for WAP protocol`);
t.writeUint8(254), t.writeUint32(e);
}
}
function R(e: any, t: Binary) {
var w = null;
if ('' === e) return t.writeUint8(252), void t.writeUint8(0);
var b = SINGLE_BYTE_TOKEN_MAP;
var r = b.get(e);
var c = [236, 237, 238, 239];
if (null == r) {
if (null == w) {
w = [];
for (var a = 0; a < DICTIONARIES_MAP.length; ++a) w.push(DICTIONARIES_MAP[a]);
}
for (var n = 0; n < w.length; ++n) {
var s = w[n].get(e);
if (null != s) return t.writeUint8(c[n]), void t.writeUint8(s);
}
var o = numUtf8Bytes(e);
if (o < 128) {
if (!/[^0-9.-]+?/.exec(e)) return void D(e, 255, t);
if (!/[^0-9A-F]+?/.exec(e)) return void D(e, 251, t);
}
N(o, t), t.writeString(e);
} else t.writeUint8(r + 1);
}
function M(e: any, t: Binary) {
var p = 248;
var f = 249;
if (void 0 === e.tag) return t.writeUint8(p), void t.writeUint8(0);
var r = 1;
e.attrs && (r += 2 * Object.keys(e.attrs).length),
e.content && r++,
r < 256 ? (t.writeUint8(p), t.writeUint8(r)) : r < 65536 && (t.writeUint8(f), t.writeUint16(r)),
O(e.tag, t),
e.attrs &&
Object.keys(e.attrs).forEach((r) => {
R(r, t), O(e.attrs[r], t);
});
var a = e.content;
if (Array.isArray(a)) {
a.length < 256 ? (t.writeUint8(p), t.writeUint8(a.length)) : a.length < 65536 && (t.writeUint8(f), t.writeUint16(a.length));
for (var i = 0; i < a.length; i++) M(a[i], t);
} else a && O(a, t);
}
function L(data: Binary, t: boolean) {
const n = data.readUint8();
if (n === 0) {
return null;
}
if (n === 248) {
return k(data, data.readUint8());
}
if (n === 249) {
return k(data, data.readUint16());
}
if (n === 252) {
return t ? data.readString(data.readUint8()) : data.readByteArray(data.readUint8());
}
if (n === 253) {
const size = ((15 & data.readUint8()) << 16) + (data.readUint8() << 8) + data.readUint8();
return t ? data.readString(size) : data.readByteArray(size);
}
if (n === 254) {
return t ? data.readString(data.readUint32()) : data.readByteArray(data.readUint32());
}
if (n === 250) {
const user = L(data, true);
if (null != user && 'string' != typeof user) throw new Error(`Decode string got invalid value ${String(t)}, string expected`);
const server = decodeStanzaString(data)
return jidEncode(user, server)
}
if (n === 247) {
const agent = data.readUint8();
const device = data.readUint8();
const user = decodeStanzaString(data);
return jidEncode(user, 's.whatsapp.net', device, agent);
}
if (n === 255) {
const number = data.readUint8();
return x(data, LIST1, number >>> 7, 127 & number);
}
if (n === 251) {
const number = data.readUint8();
return x(data, LIST2, number >>> 7, 127 & number);
}
if (n <= 0 || n >= 240) {
throw new Error('Unable to decode WAP buffer');
}
if (n >= 236 && n <= 239) {
const dict = DICTIONARIES[n - 236];
if (!dict) {
throw new Error(`Missing WAP dictionary ${n - 236}`);
}
const index = data.readUint8();
const value = dict[index];
if (!value) {
throw new Error(`Invalid value index ${index} in dict ${n - 236}`);
}
return value;
}
const singleToken = SINGLE_BYTE_TOKEN[n - 1];
if (!singleToken) throw new Error(`Undefined token with index ${n}`);
return singleToken;
}
function O(e: any, t: Binary) {
if (null == e) t.writeUint8(0);
else if (typeof e === 'object' && !(e instanceof Uint8Array) && !Buffer.isBuffer(e) && !Array.isArray(e)) M(e, t);
else if ('string' == typeof e) {
const jid = jidDecode(e)
if(jid) {
if(typeof jid.agent !== 'undefined' || typeof jid.device !== 'undefined') {
var { user: a, agent: i, device: n } = jid;
t.writeUint8(247), t.writeUint8(i || 0), t.writeUint8(n || 0), O(a, t);
} else {
var { user: s, server: l } = jid;
t.writeUint8(250), null != s ? O(s, t) : t.writeUint8(0), O(l, t);
}
} else {
R(e, t);
}
} else {
if (!(e instanceof Uint8Array)) throw new Error('Invalid payload type ' + typeof e);
N(e.length, t), t.writeByteArray(e);
}
}
function decodeStanzaString(data: Binary) {
// G
const t = L(data, true);
if (typeof t != 'string') {
throw new Error(`Decode string got invalid value ${String(t)}, string expected`);
}
return t;
}
function bufferToUInt(e: Uint8Array | Buffer, t: number) {
let a = 0
for (let i = 0; i < t; i++) a = 256 * a + e[i]
return a
}
export const decodeBinaryNode = (data: Binary): BinaryNode => {
//U
let r = data.readUint8();
let t = r === 248 ? data.readUint8() : data.readUint16();
if (!t) {
throw new Error('Failed to decode node, list cannot be empty');
}
const a = {};
const n = decodeStanzaString(data);
for (t -= 1; t > 1; ) {
const s = decodeStanzaString(data);
const l = L(data, true);
a[s] = l;
t -= 2;
}
let i = null;
1 === t && jidDecode(i = L(data, !1)) && (i = String(i));
return {
tag: n,
attrs: a,
content: i
}
}
export const encodeBinaryNode = (node: BinaryNode) => {
const data = new Binary();
O(node, data);
const dataArr = data.readByteArray();
const result = new Uint8Array(1 + dataArr.length);
result[0] = 0;
result.set(dataArr, 1);
return result;
}
// some extra useful utilities
export const getBinaryNodeChildren = ({ content }: BinaryNode, childTag: string) => {
if(Array.isArray(content)) {
return content.filter(item => item.tag == childTag)
}
return []
}
export const getAllBinaryNodeChildren = ({ content }: BinaryNode) => {
if(Array.isArray(content)) {
return content
}
return []
}
export const getBinaryNodeChild = ({ content }: BinaryNode, childTag: string) => {
if(Array.isArray(content)) {
return content.find(item => item.tag == childTag)
}
}
export const getBinaryNodeChildBuffer = (node: BinaryNode, childTag: string) => {
const child = getBinaryNodeChild(node, childTag)?.content
if(Buffer.isBuffer(child) || child instanceof Uint8Array) {
return child
}
}
export const getBinaryNodeChildUInt = (node: BinaryNode, childTag: string, length: number) => {
const buff = getBinaryNodeChildBuffer(node, childTag)
if(buff) return bufferToUInt(buff, length)
}
export const assertNodeErrorFree = (node: BinaryNode) => {
const errNode = getBinaryNodeChild(node, 'error')
if(errNode) {
throw new Boom(errNode.attrs.text || 'Unknown error', { data: +errNode.attrs.code })
}
}
export const reduceBinaryNodeToDictionary = (node: BinaryNode, tag: string) => {
const nodes = getBinaryNodeChildren(node, tag)
const dict = nodes.reduce(
(dict, { attrs }) => {
dict[attrs.name || attrs.config_code] = attrs.value || attrs.config_value
return dict
}, { } as { [_: string]: string }
)
return dict
}
export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
const msgs: proto.WebMessageInfo[] = []
if(Array.isArray(content)) {
for(const item of content) {
if(item.tag === 'message') {
msgs.push(proto.WebMessageInfo.decode(item.content as Buffer))
}
}
}
return msgs
}
export * from './encode'
export * from './decode'
export * from './generic-utils'
export * from './jid-utils'
export { Binary } from '../../WABinary/Binary'
export * from './types'
export * from './Legacy'