From b645214926a1f9952116f90d0bfe578498fa25e7 Mon Sep 17 00:00:00 2001 From: Adhiraj Date: Sat, 5 Sep 2020 19:28:35 +0530 Subject: [PATCH] Better media conn handling --- src/BrowserMessageDecoding.ts | 24 ++-------- src/Tests/Tests.Messages.ts | 11 +++++ src/WAConnection/0.Base.ts | 3 ++ src/WAConnection/3.Connect.ts | 6 +-- src/WAConnection/6.MessagesSend.ts | 23 +++++---- src/WAConnection/Constants.ts | 8 ++++ src/WAConnection/Utils.ts | 77 +++++++++++++++--------------- 7 files changed, 82 insertions(+), 70 deletions(-) diff --git a/src/BrowserMessageDecoding.ts b/src/BrowserMessageDecoding.ts index cac55c7..3dd50d4 100644 --- a/src/BrowserMessageDecoding.ts +++ b/src/BrowserMessageDecoding.ts @@ -25,30 +25,16 @@ entries.forEach ((e, i) => { wsMessages.push (...e['_webSocketMessages']) } }) -const decrypt = buffer => { - try { - return decryptWA (buffer, macKey, encKey, new Decoder()) - } catch { - return decryptWA (buffer, macKey, encKey, new Decoder(), true) - } -} +const decrypt = (buffer, fromMe) => decryptWA (buffer, macKey, encKey, new Decoder(), fromMe) console.log ('parsing ' + wsMessages.length + ' messages') 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 { - - const [tag, json, binaryTags] = decrypt (buffer) - return {tag, json: JSON.stringify(json), binaryTags} + const [tag, json, binaryTags] = decrypt (buffer, item.type === 'send') + return {tag, json: json && JSON.stringify(json), binaryTags} } catch (error) { - try { - 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 - } - + return { error: error.message, data: buffer.toString('utf-8') } } }) const str = JSON.stringify (list, null, '\t') diff --git a/src/Tests/Tests.Messages.ts b/src/Tests/Tests.Messages.ts index 860cf5f..3fb84ba 100644 --- a/src/Tests/Tests.Messages.ts +++ b/src/Tests/Tests.Messages.ts @@ -83,6 +83,17 @@ WAConnectionTest('Messages', conn => { await delay (2000) 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 => { const JID = '1234-1234@g.us' conn.sendMessage(JID, 'hello', MessageType.text) diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 6a5b42b..9944f1b 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -20,6 +20,7 @@ import { WAQuery, ReconnectMode, WAConnectOptions, + MediaConnInfo, } from './Constants' import { EventEmitter } from 'events' import KeyedDB from '@adiwajshing/keyed-db' @@ -74,6 +75,8 @@ export class WAConnection extends EventEmitter { protected lastDisconnectTime: Date = null protected lastDisconnectReason: DisconnectReason + protected mediaConn: MediaConnInfo + constructor () { super () this.registerCallback (['Cmd', 'type:disconnect'], json => ( diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index c23efa9..52b8ed0 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -266,10 +266,8 @@ export class WAConnection extends Base { this.lastSeen = new Date(parseInt(timestamp)) this.emit ('received-pong') } else { - const decrypted = Utils.decryptWA (message, this.authInfo?.macKey, this.authInfo?.encKey, new Decoder()) - if (!decrypted) return - - const [messageTag, json] = decrypted + const [messageTag, json] = Utils.decryptWA (message, this.authInfo?.macKey, this.authInfo?.encKey, new Decoder()) + if (!json) return if (this.logLevel === MessageLogLevel.all) { this.log(messageTag + ', ' + JSON.stringify(json), MessageLogLevel.all) diff --git a/src/WAConnection/6.MessagesSend.ts b/src/WAConnection/6.MessagesSend.ts index 8b1ddd4..4f3495e 100644 --- a/src/WAConnection/6.MessagesSend.ts +++ b/src/WAConnection/6.MessagesSend.ts @@ -10,7 +10,7 @@ import { WALocationMessage, WAContactMessage, 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' import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds } from './Utils' @@ -105,14 +105,12 @@ export class WAConnection extends Base { .replace(/\=+$/, '') 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, { method: 'POST', body: body, @@ -232,4 +230,13 @@ export class WAConnection extends Base { await fs.writeFile (trueFileName, buffer) 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 + } } diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index d27d1e4..a97a0e4 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -97,6 +97,14 @@ export enum MessageLogLevel { unhandled=2, all=3 } +export interface MediaConnInfo { + auth: string + ttl: number + hosts: { + hostname: string + }[] + fetchDate: Date +} export interface AuthenticationCredentials { clientID: string serverToken: string diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index 45dfc87..45ce5c5 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -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]?] { 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 (message[commaIndex+1] === ',') commaIndex += 1 let data = message.slice(commaIndex+1, message.length) + // get the message tag. // 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() - if (data.length === 0) { - // got an empty message, usually get one after sending a query with the 128 tag - return - } - let json - let tags = null - if (typeof data === 'string') { - json = JSON.parse(data) // parse the JSON - } else { - if (!macKey || !encKey) { - 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 (fromMe) { - 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 computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey - - if (!checksum.equals(computedChecksum)) { - console.error (` - Checksums don't match: - og: ${checksum.toString('hex')} - computed: ${computedChecksum.toString('hex')} - message: ${message.slice(0, 80).toString()} - `) - return - } - // the checksum the server sent, must match the one we computed for the message to be valid - const decrypted = aesDecrypt(data, encKey) // decrypt using AES - json = decoder.read(decrypted) // decode the binary message into a JSON array + let tags + if (data.length > 0) { + if (typeof data === 'string') { + json = JSON.parse(data) // parse the JSON + } else { + if (!macKey || !encKey) { + 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 (fromMe) { + 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 computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey + + if (checksum.equals(computedChecksum)) { + // the checksum the server sent, must match the one we computed for the message to be valid + const decrypted = aesDecrypt(data, encKey) // decrypt using AES + json = decoder.read(decrypted) // decode the binary message into a JSON array + } else { + console.error (` + Checksums don't match: + og: ${checksum.toString('hex')} + computed: ${computedChecksum.toString('hex')} + data: ${data.slice(0, 80).toString()} + tag: ${messageTag} + message: ${message.slice(0, 80).toString()} + `) + } + } } return [messageTag, json, tags] }