Better media conn handling

This commit is contained in:
Adhiraj
2020-09-05 19:28:35 +05:30
parent 9041272b5c
commit b645214926
7 changed files with 82 additions and 70 deletions

View File

@@ -25,30 +25,16 @@ entries.forEach ((e, i) => {
wsMessages.push (...e['_webSocketMessages']) wsMessages.push (...e['_webSocketMessages'])
} }
}) })
const decrypt = buffer => { const decrypt = (buffer, fromMe) => decryptWA (buffer, macKey, encKey, new Decoder(), fromMe)
try {
return decryptWA (buffer, macKey, encKey, new Decoder())
} catch {
return decryptWA (buffer, macKey, encKey, new Decoder(), true)
}
}
console.log ('parsing ' + wsMessages.length + ' messages') console.log ('parsing ' + wsMessages.length + ' messages')
const list = wsMessages.map ((item, i) => { const list = wsMessages.map ((item, i) => {
const buffer = Buffer.from (item.data, 'base64') const buffer = item.data.includes(',') ? item.data : Buffer.from (item.data, 'base64')
try { try {
const [tag, json, binaryTags] = decrypt (buffer, item.type === 'send')
const [tag, json, binaryTags] = decrypt (buffer) return {tag, json: json && JSON.stringify(json), binaryTags}
return {tag, json: JSON.stringify(json), binaryTags}
} catch (error) { } catch (error) {
try { return { error: error.message, data: buffer.toString('utf-8') }
const [tag, json, binaryTags] = decrypt (item.data)
return {tag, json: JSON.stringify(json), binaryTags}
} catch (error) {
console.log ('error in decoding: ' + item.data + ': ' + error)
return null
}
} }
}) })
const str = JSON.stringify (list, null, '\t') const str = JSON.stringify (list, null, '\t')

View File

@@ -83,6 +83,17 @@ WAConnectionTest('Messages', conn => {
await delay (2000) await delay (2000)
await conn.clearMessage (messages[0].key) await conn.clearMessage (messages[0].key)
}) })
it('should send media after close', async () => {
const content = await fs.readFile('./Media/octopus.webp')
await sendAndRetreiveMessage(conn, content, MessageType.sticker)
conn.close ()
await conn.connect ()
const content2 = await fs.readFile('./Media/cat.jpeg')
await sendAndRetreiveMessage(conn, content2, MessageType.image)
})
it('should fail to send a text message', done => { it('should fail to send a text message', done => {
const JID = '1234-1234@g.us' const JID = '1234-1234@g.us'
conn.sendMessage(JID, 'hello', MessageType.text) conn.sendMessage(JID, 'hello', MessageType.text)

View File

@@ -20,6 +20,7 @@ import {
WAQuery, WAQuery,
ReconnectMode, ReconnectMode,
WAConnectOptions, WAConnectOptions,
MediaConnInfo,
} from './Constants' } from './Constants'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import KeyedDB from '@adiwajshing/keyed-db' import KeyedDB from '@adiwajshing/keyed-db'
@@ -74,6 +75,8 @@ export class WAConnection extends EventEmitter {
protected lastDisconnectTime: Date = null protected lastDisconnectTime: Date = null
protected lastDisconnectReason: DisconnectReason protected lastDisconnectReason: DisconnectReason
protected mediaConn: MediaConnInfo
constructor () { constructor () {
super () super ()
this.registerCallback (['Cmd', 'type:disconnect'], json => ( this.registerCallback (['Cmd', 'type:disconnect'], json => (

View File

@@ -266,10 +266,8 @@ export class WAConnection extends Base {
this.lastSeen = new Date(parseInt(timestamp)) this.lastSeen = new Date(parseInt(timestamp))
this.emit ('received-pong') this.emit ('received-pong')
} else { } else {
const decrypted = Utils.decryptWA (message, this.authInfo?.macKey, this.authInfo?.encKey, new Decoder()) const [messageTag, json] = Utils.decryptWA (message, this.authInfo?.macKey, this.authInfo?.encKey, new Decoder())
if (!decrypted) return if (!json) return
const [messageTag, json] = decrypted
if (this.logLevel === MessageLogLevel.all) { if (this.logLevel === MessageLogLevel.all) {
this.log(messageTag + ', ' + JSON.stringify(json), MessageLogLevel.all) this.log(messageTag + ', ' + JSON.stringify(json), MessageLogLevel.all)

View File

@@ -10,7 +10,7 @@ import {
WALocationMessage, WALocationMessage,
WAContactMessage, WAContactMessage,
WATextMessage, WATextMessage,
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE, WAMessageProto WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo
} from './Constants' } from './Constants'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds } from './Utils' import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds } from './Utils'
@@ -105,14 +105,12 @@ export class WAConnection extends Base {
.replace(/\=+$/, '') .replace(/\=+$/, '')
await generateThumbnail(buffer, mediaType, options) await generateThumbnail(buffer, mediaType, options)
// send a query JSON to obtain the url & auth token to upload our media
const json = (await this.query({json: ['query', 'mediaConn']})).media_conn
const auth = json.auth // the auth token
let hostname = 'https://' + json.hosts[0].hostname // first hostname available
hostname += MediaPathMap[mediaType] + '/' + fileEncSha256B64 // append path
hostname += '?auth=' + auth // add auth token
hostname += '&token=' + fileEncSha256B64 // file hash
// send a query JSON to obtain the url & auth token to upload our media
const json = await this.refreshMediaConn ()
const auth = json.auth // the auth token
const hostname = `https://${json.hosts[0].hostname}${MediaPathMap[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
const urlFetch = await fetch(hostname, { const urlFetch = await fetch(hostname, {
method: 'POST', method: 'POST',
body: body, body: body,
@@ -232,4 +230,13 @@ export class WAConnection extends Base {
await fs.writeFile (trueFileName, buffer) await fs.writeFile (trueFileName, buffer)
return trueFileName return trueFileName
} }
protected async refreshMediaConn () {
if (!this.mediaConn || (new Date().getTime()-this.mediaConn.fetchDate.getTime()) > this.mediaConn.ttl*1000) {
const result = await this.query({json: ['query', 'mediaConn']})
this.mediaConn = result.media_conn
this.mediaConn.fetchDate = new Date()
}
return this.mediaConn
}
} }

View File

@@ -97,6 +97,14 @@ export enum MessageLogLevel {
unhandled=2, unhandled=2,
all=3 all=3
} }
export interface MediaConnInfo {
auth: string
ttl: number
hosts: {
hostname: string
}[]
fetchDate: Date
}
export interface AuthenticationCredentials { export interface AuthenticationCredentials {
clientID: string clientID: string
serverToken: string serverToken: string

View File

@@ -130,53 +130,52 @@ export function generateMessageID() {
} }
export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buffer, decoder: Decoder, fromMe: boolean=false): [string, Object, [number, number]?] { export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buffer, decoder: Decoder, fromMe: boolean=false): [string, Object, [number, number]?] {
let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
if (commaIndex < 0) throw Error ('invalid message: ' + message) // if there was no comma, then this message must be not be valid if (commaIndex < 0) throw Error ('invalid message: ' + message) // if there was no comma, then this message must be not be valid
if (message[commaIndex+1] === ',') commaIndex += 1 if (message[commaIndex+1] === ',') commaIndex += 1
let data = message.slice(commaIndex+1, message.length) let data = message.slice(commaIndex+1, message.length)
// get the message tag. // get the message tag.
// If a query was done, the server will respond with the same message tag we sent the query with // If a query was done, the server will respond with the same message tag we sent the query with
const messageTag: string = message.slice(0, commaIndex).toString() const messageTag: string = message.slice(0, commaIndex).toString()
if (data.length === 0) {
// got an empty message, usually get one after sending a query with the 128 tag
return
}
let json let json
let tags = null let tags
if (typeof data === 'string') { if (data.length > 0) {
json = JSON.parse(data) // parse the JSON if (typeof data === 'string') {
} else { json = JSON.parse(data) // parse the JSON
if (!macKey || !encKey) { } else {
console.warn ('recieved encrypted buffer when auth creds unavailable: ' + message) if (!macKey || !encKey) {
return console.warn ('recieved encrypted buffer when auth creds unavailable: ' + message)
} return
/* }
If the data recieved was not a JSON, then it must be an encrypted message. /*
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys If the data recieved was not a JSON, then it must be an encrypted message.
*/ Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
if (fromMe) { */
tags = [data[0], data[1]] if (fromMe) {
data = data.slice(2, data.length) tags = [data[0], data[1]]
} data = data.slice(2, data.length)
}
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
data = data.slice(32, data.length) // the actual message const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey data = data.slice(32, data.length) // the actual message
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey
if (!checksum.equals(computedChecksum)) {
console.error (` if (checksum.equals(computedChecksum)) {
Checksums don't match: // the checksum the server sent, must match the one we computed for the message to be valid
og: ${checksum.toString('hex')} const decrypted = aesDecrypt(data, encKey) // decrypt using AES
computed: ${computedChecksum.toString('hex')} json = decoder.read(decrypted) // decode the binary message into a JSON array
message: ${message.slice(0, 80).toString()} } else {
`) console.error (`
return Checksums don't match:
} og: ${checksum.toString('hex')}
// the checksum the server sent, must match the one we computed for the message to be valid computed: ${computedChecksum.toString('hex')}
const decrypted = aesDecrypt(data, encKey) // decrypt using AES data: ${data.slice(0, 80).toString()}
json = decoder.read(decrypted) // decode the binary message into a JSON array tag: ${messageTag}
message: ${message.slice(0, 80).toString()}
`)
}
}
} }
return [messageTag, json, tags] return [messageTag, json, tags]
} }