Added logger, handled checksum fail

This commit is contained in:
Adhiraj Singh
2020-10-12 20:32:32 +05:30
parent 3fa2db4864
commit 6c000ab093
12 changed files with 342 additions and 101 deletions

View File

@@ -108,6 +108,34 @@ describe ('Reconnects', () => {
conn.close ()
}
it('should dispose correctly on bad_session', async () => {
const conn = new WAConnection()
conn.autoReconnect = ReconnectMode.onAllErrors
conn.loadAuthInfo ('./auth_info.json')
let gotClose0 = false
let gotClose1 = false
conn.on ('intermediate-close', ({ reason }) => {
gotClose0 = true
})
conn.on ('close', ({ reason }) => {
if (reason === DisconnectReason.badSession) gotClose1 = true
})
setTimeout (() => conn['conn'].emit ('message', Buffer.from('some-tag,sdjjij1jo2ejo1je')), 1500)
await conn.connect ()
setTimeout (() => conn['conn'].emit ('message', Buffer.from('some-tag,sdjjij1jo2ejo1je')), 1500)
await new Promise (resolve => {
conn.on ('open', resolve)
})
assert.ok (gotClose0, 'did not receive bad_session close initially')
assert.ok (gotClose1, 'did not receive bad_session close')
conn.close ()
})
/**
* the idea is to test closing the connection at multiple points in the connection
* and see if the library cleans up resources correctly

View File

@@ -9,7 +9,6 @@ import {
WAUser,
WANode,
WATag,
MessageLogLevel,
BaileysError,
WAMetric,
WAFlag,
@@ -27,6 +26,9 @@ import {
import { EventEmitter } from 'events'
import KeyedDB from '@adiwajshing/keyed-db'
import { STATUS_CODES, Agent } from 'http'
import pino from 'pino'
const logger = pino({ prettyPrint: { levelFirst: true, ignore: 'hostname', translateTime: true }, prettifier: require('pino-pretty') })
export class WAConnection extends EventEmitter {
/** The version of WhatsApp Web we're telling the servers we are */
@@ -35,8 +37,6 @@ export class WAConnection extends EventEmitter {
browserDescription: [string, string, string] = Utils.Browsers.baileys ('Chrome')
/** Metadata like WhatsApp id, name set on WhatsApp etc. */
user: WAUser
/** What level of messages to log to the console */
logLevel: MessageLogLevel = MessageLogLevel.info
/** Should requests be queued when the connection breaks in between; if 0, then an error will be thrown */
pendingRequestTimeoutMs: number = null
/** The connection state */
@@ -57,6 +57,8 @@ export class WAConnection extends EventEmitter {
/** key to use to order chats */
chatOrderingKey = Utils.waChatKey(false)
logger = logger.child ({ class: 'Baileys' })
/** log messages */
shouldLogMessages = false
messageLog: { tag: string, json: string, fromMe: boolean, binaryTags?: any[] }[] = []
@@ -141,7 +143,7 @@ export class WAConnection extends EventEmitter {
if (!authInfo) throw new Error('given authInfo is null')
if (typeof authInfo === 'string') {
this.log(`loading authentication credentials from ${authInfo}`, MessageLogLevel.info)
this.logger.info(`loading authentication credentials from ${authInfo}`)
const file = fs.readFileSync(authInfo, { encoding: 'utf-8' }) // load a closed session back if it exists
authInfo = JSON.parse(file) as AnyAuthenticationCredentials
}
@@ -205,7 +207,7 @@ export class WAConnection extends EventEmitter {
delete this.callbacks[func][key][key2]
return
}
this.log('WARNING: could not find ' + JSON.stringify(parameters) + ' to deregister', MessageLogLevel.info)
this.logger.warn('Could not find ' + JSON.stringify(parameters) + ' to deregister')
}
/**
* Wait for a message with a certain tag to be received
@@ -330,7 +332,7 @@ export class WAConnection extends EventEmitter {
this.closeInternal (DisconnectReason.intentional)
}
protected closeInternal (reason?: DisconnectReason, isReconnecting: boolean=false) {
this.log (`closed connection, reason ${reason}${isReconnecting ? ', reconnecting in a few seconds...' : ''}`, MessageLogLevel.info)
this.logger.info (`closed connection, reason ${reason}${isReconnecting ? ', reconnecting in a few seconds...' : ''}`)
this.qrTimeout && clearTimeout (this.qrTimeout)
this.debounceTimeout && clearTimeout (this.debounceTimeout)
@@ -368,7 +370,7 @@ export class WAConnection extends EventEmitter {
Object.keys(this.callbacks).forEach(key => {
if (!key.startsWith('function:')) {
this.log (`cancelling message wait: ${key}`, MessageLogLevel.info)
this.logger.trace (`cancelling message wait: ${key}`)
this.callbacks[key].errCallback(new Error('close'))
delete this.callbacks[key]
}
@@ -389,7 +391,7 @@ export class WAConnection extends EventEmitter {
const seconds = Utils.unixTimestampSeconds(this.referenceDate)
return `${longTag ? seconds : (seconds%1000)}.--${this.msgCount}`
}
protected log(text, level: MessageLogLevel) {
/*protected log(text, level: MessageLogLevel) {
(this.logLevel >= level) && console.log(`[Baileys][${new Date().toLocaleString()}] ${text}`)
}
}*/
}

View File

@@ -1,7 +1,7 @@
import * as Curve from 'curve25519-js'
import * as Utils from './Utils'
import {WAConnection as Base} from './0.Base'
import { MessageLogLevel, WAMetric, WAFlag, BaileysError, Presence, WAUser, DisconnectReason } from './Constants'
import { WAMetric, WAFlag, BaileysError, Presence, WAUser } from './Constants'
export class WAConnection extends Base {
@@ -62,14 +62,14 @@ export class WAConnection extends Base {
const validationJSON = (await Promise.all (initQueries)).slice(-1)[0] // get the last result
this.user = await this.validateNewConnection(validationJSON[1]) // validate the connection
this.log('validated connection successfully', MessageLogLevel.info)
this.logger.info('validated connection successfully')
const response = await this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false })
this.user.imgUrl = response?.eurl || ''
this.sendPostConnectQueries ()
this.log('sent init queries', MessageLogLevel.info)
this.logger.debug('sent init queries')
}
/**
* Send the same queries WA Web sends after connect
@@ -176,7 +176,8 @@ export class WAConnection extends Base {
const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string
const signed = Utils.hmacSign(bytes, this.authInfo.macKey).toString('base64') // sign the challenge string with our macKey
const json = ['admin', 'challenge', signed, this.authInfo.serverToken, this.authInfo.clientID] // prepare to send this signed string with the serverToken & clientID
this.log('resolving login challenge', MessageLogLevel.info)
this.logger.info('resolving login challenge')
return this.query({json, expect200: true, waitForOpen: false})
}
/** When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends */
@@ -193,14 +194,14 @@ export class WAConnection extends Base {
this.qrTimeout = setTimeout (() => {
if (this.state === 'open') return
this.log ('regenerated QR', MessageLogLevel.info)
this.logger.debug ('regenerated QR')
this.generateNewQRCodeRef ()
.then (newRef => ref = newRef)
.then (emitQR)
.then (regenQR)
.catch (err => {
this.log (`error in QR gen: ${err}`, MessageLogLevel.info)
this.logger.error (`error in QR gen: `, err)
if (err.status === 429) { // too many QR requests
this.endConnection ()
}

View File

@@ -1,5 +1,5 @@
import * as Utils from './Utils'
import { WAMessage, WAChat, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason, UNAUTHORIZED_CODES, WAContact, TimedOutError, CancelledError, WAOpenResult, DEFAULT_ORIGIN, WS_URL } from './Constants'
import { WAMessage, WAChat, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason, UNAUTHORIZED_CODES, WAContact, TimedOutError, CancelledError, WAOpenResult, DEFAULT_ORIGIN, WS_URL } from './Constants'
import {WAConnection as Base} from './1.Validation'
import Decoder from '../Binary/Decoder'
import WS from 'ws'
@@ -37,7 +37,7 @@ export class WAConnection extends Base {
const willReconnect = !loggedOut && (tries <= (options?.maxRetries || 5)) && this.state === 'connecting'
const reason = loggedOut ? DisconnectReason.invalidSession : error.message
this.log (`connect attempt ${tries} failed: ${error}${ willReconnect ? ', retrying...' : ''}`, MessageLogLevel.info)
this.logger.warn (`connect attempt ${tries} failed${ willReconnect ? ', retrying...' : ''}`, error)
if ((this.state as string) !== 'close' && !willReconnect) {
this.closeInternal (reason)
@@ -53,7 +53,7 @@ export class WAConnection extends Base {
this.releasePendingRequests ()
this.log ('opened connection to WhatsApp Web', MessageLogLevel.info)
this.logger.info ('opened connection to WhatsApp Web')
this.conn.on ('close', () => this.unexpectedDisconnect (DisconnectReason.close))
@@ -82,7 +82,7 @@ export class WAConnection extends Base {
this.conn.removeEventListener ('message', checkIdleTime)
}
const reconnectID = shouldUseReconnect ? this.user.jid.replace ('@s.whatsapp.net', '@c.us') : null
const reconnectID = shouldUseReconnect && this.user.jid.replace ('@s.whatsapp.net', '@c.us')
this.conn = new WS(WS_URL, null, {
origin: DEFAULT_ORIGIN,
@@ -100,7 +100,7 @@ export class WAConnection extends Base {
this.conn.addEventListener('message', ({data}) => this.onMessageRecieved(data as any))
this.conn.on ('open', async () => {
this.log(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`, MessageLogLevel.info)
this.logger.info(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`)
let waitForChats: Promise<{[k: string]: Partial<WAChat>}>
// add wait for chats promise if required
if (typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats) {
@@ -228,7 +228,7 @@ export class WAConnection extends Base {
json[2]
.forEach(([item, chat]: [any, WAChat]) => {
if (!chat) {
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
this.logger.warn (`unexpectedly got null chat: ${item}`, chat)
return
}
@@ -241,7 +241,7 @@ export class WAConnection extends Base {
!chats.get (chat.jid) && chats.insert (chat)
})
this.log (`received ${json[2].length} chats`, MessageLogLevel.info)
this.logger.info (`received ${json[2].length} chats`)
if (json[2].length === 0) {
receivedMessages = true
checkForResolution ()
@@ -254,12 +254,12 @@ export class WAConnection extends Base {
receivedContacts = true
json[2].forEach(([type, contact]: ['user', WAContact]) => {
if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info)
if (!contact) return this.logger.info (`unexpectedly got null contact: ${type}`, contact)
contact.jid = Utils.whatsappID (contact.jid)
contacts[contact.jid] = contact
})
this.log (`received ${json[2].length} contacts`, MessageLogLevel.info)
this.logger.info (`received ${json[2].length} contacts`)
checkForResolution ()
})
@@ -310,64 +310,71 @@ export class WAConnection extends Base {
this.lastSeen = new Date(parseInt(timestamp))
this.emit ('received-pong')
} else {
const [messageTag, json] = Utils.decryptWA (message, this.authInfo?.macKey, this.authInfo?.encKey, new Decoder())
if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false })
if (!json) {return}
try {
const [messageTag, json] = Utils.decryptWA (message, this.authInfo?.macKey, this.authInfo?.encKey, new Decoder())
if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false })
if (!json) { return }
if (this.logLevel === MessageLogLevel.all) {
this.log(messageTag + ', ' + JSON.stringify(json), MessageLogLevel.all)
}
if (!this.phoneConnected && this.state === 'open') {
this.phoneConnected = true
this.emit ('connection-phone-change', { connected: true })
}
/*
Check if this is a response to a message we sent
*/
if (this.callbacks[messageTag]) {
const q = this.callbacks[messageTag]
q.callback(json)
delete this.callbacks[messageTag]
return
}
/*
Check if this is a response to a message we are expecting
*/
if (this.callbacks['function:' + json[0]]) {
const callbacks = this.callbacks['function:' + json[0]]
let callbacks2
let callback
for (const key in json[1] || {}) {
callbacks2 = callbacks[key + ':' + json[1][key]]
if (callbacks2) {
break
}
if (this.logger.level === 'trace') {
this.logger.trace(messageTag + ', ' + JSON.stringify(json))
}
if (!callbacks2) {
if (!this.phoneConnected && this.state === 'open') {
this.phoneConnected = true
this.emit ('connection-phone-change', { connected: true })
}
/*
Check if this is a response to a message we sent
*/
if (this.callbacks[messageTag]) {
const q = this.callbacks[messageTag]
q.callback(json)
delete this.callbacks[messageTag]
return
}
/*
Check if this is a response to a message we are expecting
*/
if (this.callbacks['function:' + json[0]]) {
const callbacks = this.callbacks['function:' + json[0]]
let callbacks2
let callback
for (const key in json[1] || {}) {
callbacks2 = callbacks[key]
callbacks2 = callbacks[key + ':' + json[1][key]]
if (callbacks2) {
break
}
}
}
if (!callbacks2) {
callbacks2 = callbacks['']
}
if (callbacks2) {
callback = callbacks2[json[2] && json[2][0][0]]
if (!callback) {
callback = callbacks2['']
if (!callbacks2) {
for (const key in json[1] || {}) {
callbacks2 = callbacks[key]
if (callbacks2) {
break
}
}
}
if (!callbacks2) {
callbacks2 = callbacks['']
}
if (callbacks2) {
callback = callbacks2[json[2] && json[2][0][0]]
if (!callback) {
callback = callbacks2['']
}
}
if (callback) {
callback(json)
return
}
}
if (callback) {
callback(json)
return
if (this.logger.level === 'debug') {
this.logger.debug({ unhandled: true }, messageTag + ', ' + JSON.stringify(json))
}
}
if (this.logLevel === MessageLogLevel.unhandled) {
this.log('[Unhandled] ' + messageTag + ', ' + JSON.stringify(json), MessageLogLevel.unhandled)
} catch (error) {
this.logger.error (`encountered error in decrypting message, closing`, error)
if (this.state === 'open') this.unexpectedDisconnect (DisconnectReason.badSession)
else this.endConnection ()
}
}
}

View File

@@ -1,6 +1,6 @@
import * as QR from 'qrcode-terminal'
import { WAConnection as Base } from './3.Connect'
import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, MessageLogLevel, PresenceUpdate, BaileysEvent, DisconnectReason, WANode, WAOpenResult, Presence, AuthenticationCredentials } from './Constants'
import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, PresenceUpdate, BaileysEvent, DisconnectReason, WANode, WAOpenResult, Presence, AuthenticationCredentials } from './Constants'
import { whatsappID, unixTimestampSeconds, isGroupID, toNumber, GET_MESSAGE_ID, WA_MESSAGE_ID, waMessageKey } from './Utils'
import KeyedDB from '@adiwajshing/keyed-db'
import { Mutex } from './Mutex'
@@ -236,7 +236,7 @@ export class WAConnection extends Base {
case WAMessageProto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE:
const found = chat.messages.get (GET_MESSAGE_ID(protocolMessage.key))
if (found?.message) {
this.log ('deleting message: ' + protocolMessage.key.id + ' in chat: ' + protocolMessage.key.remoteJid, MessageLogLevel.info)
this.logger.info ('deleting message: ' + protocolMessage.key.id + ' in chat: ' + protocolMessage.key.remoteJid)
found.messageStubType = WA_MESSAGE_STUB_TYPE.REVOKE
delete found.message

View File

@@ -9,7 +9,7 @@ import {
WALocationMessage,
WAContactMessage,
WATextMessage,
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto, URL_REGEX, WAUrlInfo
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto, URL_REGEX, WAUrlInfo
} from './Constants'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds } from './Utils'
import { Mutex } from './Mutex'
@@ -138,7 +138,7 @@ export class WAConnection extends Base {
}
} catch (error) {
const isLast = host.hostname === json.hosts[json.hosts.length-1].hostname
this.log (`Error in uploading to ${host.hostname}${isLast ? '' : ', retrying...'}`, MessageLogLevel.info)
this.logger.error (`Error in uploading to ${host.hostname}${isLast ? '' : ', retrying...'}`)
}
}
if (!mediaUrl) throw new Error('Media upload failed on all hosts')
@@ -241,7 +241,7 @@ export class WAConnection extends Base {
return buff
} catch (error) {
if (error instanceof BaileysError && error.status === 404) { // media needs to be updated
this.log (`updating media of message: ${message.key.id}`, MessageLogLevel.info)
this.logger.info (`updating media of message: ${message.key.id}`)
await this.updateMediaMessage (message)
const buff = await decodeMediaMessageBuffer (message.message, this.fetchRequest)
return buff

View File

@@ -1,5 +1,5 @@
import {WAConnection as Base} from './7.MessagesExtra'
import { WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification, MessageLogLevel } from '../WAConnection/Constants'
import { WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants'
import { GroupSettingChange } from './Constants'
import { generateMessageID } from '../WAConnection/Utils'
@@ -52,13 +52,13 @@ export class WAConnection extends Base {
try {
await this.groupMetadata (gid)
} catch (error) {
this.log (`error in group creation: ${error}, switching gid & checking`, MessageLogLevel.info)
this.logger.warn (`error in group creation: ${error}, switching gid & checking`)
// if metadata is not available
const comps = gid.replace ('@g.us', '').split ('-')
response.gid = `${comps[0]}-${+comps[1] + 1}@g.us`
await this.groupMetadata (gid)
this.log (`group ID switched from ${gid} to ${response.gid}`, MessageLogLevel.info)
this.logger.warn (`group ID switched from ${gid} to ${response.gid}`)
}
await this.chatAdd (response.gid, title)
return response

View File

@@ -114,12 +114,6 @@ export enum DisconnectReason {
/** Well, the connection timed out */
timedOut = 'timed out'
}
export enum MessageLogLevel {
none=0,
info=1,
unhandled=2,
all=3
}
export interface MediaConnInfo {
auth: string
ttl: number

View File

@@ -152,8 +152,7 @@ export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buf
json = JSON.parse(data) // parse the JSON
} else {
if (!macKey || !encKey) {
console.warn ('recieved encrypted buffer when auth creds unavailable: ' + message)
return
throw new Error ('recieved encrypted buffer when auth creds unavailable: ' + message)
}
/*
If the data recieved was not a JSON, then it must be an encrypted message.
@@ -173,14 +172,13 @@ export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buf
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()}
`)
throw new BaileysError ('checksum failed', {
received: checksum.toString('hex'),
computed: computedChecksum.toString('hex'),
data: data.slice(0, 80).toString(),
tag: messageTag,
message: message.slice(0, 80).toString()
})
}
}
}