mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
feat: native-mobile-api
This commit is contained in:
@@ -4,22 +4,36 @@ import type { AuthenticationState, MediaType, SocketConfig, WAVersion } from '..
|
||||
import { Browsers } from '../Utils'
|
||||
import logger from '../Utils/logger'
|
||||
import { version } from './baileys-version.json'
|
||||
import phoneNumberMCC from './phonenumber-mcc.json'
|
||||
|
||||
export const UNAUTHORIZED_CODES = [401, 403, 419]
|
||||
|
||||
export const PHONENUMBER_MCC = phoneNumberMCC
|
||||
|
||||
export const DEFAULT_ORIGIN = 'https://web.whatsapp.com'
|
||||
export const MOBILE_ENDPOINT = 'g.whatsapp.net'
|
||||
export const MOBILE_PORT = 443
|
||||
export const DEF_CALLBACK_PREFIX = 'CB:'
|
||||
export const DEF_TAG_PREFIX = 'TAG:'
|
||||
export const PHONE_CONNECTION_CB = 'CB:Pong'
|
||||
|
||||
export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60
|
||||
|
||||
export const MOBILE_TOKEN = Buffer.from('0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM4174c0243f5277a5d7720ce842cc4ae6')
|
||||
export const MOBILE_REGISTRATION_ENDPOINT = 'https://v.whatsapp.net/v2'
|
||||
export const MOBILE_USERAGENT = 'WhatsApp/2.22.24.81 iOS/15.3.1 Device/Apple-iPhone_7'
|
||||
export const REGISTRATION_PUBLIC_KEY = Buffer.from([
|
||||
5, 142, 140, 15, 116, 195, 235, 197, 215, 166, 134, 92, 108, 60, 132, 56, 86, 176, 97, 33, 204, 232, 234, 119, 77,
|
||||
34, 251, 111, 18, 37, 18, 48, 45,
|
||||
])
|
||||
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
|
||||
export const DICT_VERSION = 2
|
||||
export const KEY_BUNDLE_TYPE = Buffer.from([5])
|
||||
export const NOISE_WA_HEADER = Buffer.from(
|
||||
[ 87, 65, 6, DICT_VERSION ]
|
||||
) // last is "DICT_VERSION"
|
||||
export const PROTOCOL_VERSION = [5, 2]
|
||||
export const MOBILE_NOISE_HEADER = Buffer.concat([Buffer.from('WA'), Buffer.from(PROTOCOL_VERSION)])
|
||||
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
|
||||
export const URL_REGEX = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/
|
||||
export const URL_EXCLUDE_REGEX = /.*@.*/
|
||||
|
||||
223
src/Defaults/phonenumber-mcc.json
Normal file
223
src/Defaults/phonenumber-mcc.json
Normal file
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"93": 412,
|
||||
"355": 276,
|
||||
"213": 603,
|
||||
"1-684": 544,
|
||||
"376": 213,
|
||||
"244": 631,
|
||||
"1-264": 365,
|
||||
"1-268": 344,
|
||||
"54": 722,
|
||||
"374": 283,
|
||||
"297": 363,
|
||||
"61": 505,
|
||||
"43": 232,
|
||||
"994": 400,
|
||||
"1-242": 364,
|
||||
"973": 426,
|
||||
"880": 470,
|
||||
"1-246": 342,
|
||||
"375": 257,
|
||||
"32": 206,
|
||||
"501": 702,
|
||||
"229": 616,
|
||||
"1-441": 350,
|
||||
"975": 402,
|
||||
"591": 736,
|
||||
"387": 218,
|
||||
"267": 652,
|
||||
"55": 724,
|
||||
"1-284": 348,
|
||||
"673": 528,
|
||||
"359": 284,
|
||||
"226": 613,
|
||||
"257": 642,
|
||||
"855": 456,
|
||||
"237": 624,
|
||||
"238": 625,
|
||||
"1-345": 346,
|
||||
"236": 623,
|
||||
"235": 622,
|
||||
"56": 730,
|
||||
"86": 454,
|
||||
"57": 732,
|
||||
"269": 654,
|
||||
"682": 548,
|
||||
"506": 712,
|
||||
"385": 219,
|
||||
"53": 368,
|
||||
"357": 280,
|
||||
"420": 230,
|
||||
"243": 630,
|
||||
"45": 238,
|
||||
"253": 638,
|
||||
"1-767": 366,
|
||||
"1-809": 370,
|
||||
"1-849": 370,
|
||||
"1-829": 370,
|
||||
"593": 740,
|
||||
"20": 602,
|
||||
"503": 706,
|
||||
"240": 627,
|
||||
"291": 657,
|
||||
"372": 248,
|
||||
"251": 636,
|
||||
"500": 750,
|
||||
"298": 288,
|
||||
"679": 542,
|
||||
"358": 244,
|
||||
"33": 208,
|
||||
"689": 547,
|
||||
"241": 628,
|
||||
"220": 607,
|
||||
"995": 282,
|
||||
"49": 262,
|
||||
"233": 620,
|
||||
"350": 266,
|
||||
"30": 202,
|
||||
"299": 290,
|
||||
"1-473": 352,
|
||||
"1-671": 535,
|
||||
"502": 704,
|
||||
"224": 537,
|
||||
"592": 738,
|
||||
"509": 372,
|
||||
"504": 708,
|
||||
"852": 454,
|
||||
"36": 216,
|
||||
"354": 274,
|
||||
"91": 404,
|
||||
"62": 510,
|
||||
"98": 432,
|
||||
"964": 418,
|
||||
"353": 234,
|
||||
"972": 425,
|
||||
"39": 222,
|
||||
"225": 612,
|
||||
"1-876": 338,
|
||||
"81": 440,
|
||||
"962": 416,
|
||||
"254": 639,
|
||||
"686": 545,
|
||||
"383": 221,
|
||||
"965": 419,
|
||||
"371": 247,
|
||||
"961": 415,
|
||||
"266": 651,
|
||||
"231": 618,
|
||||
"218": 606,
|
||||
"423": 295,
|
||||
"370": 246,
|
||||
"352": 270,
|
||||
"389": 294,
|
||||
"261": 646,
|
||||
"265": 650,
|
||||
"60": 502,
|
||||
"960": 472,
|
||||
"223": 610,
|
||||
"356": 278,
|
||||
"692": 551,
|
||||
"222": 609,
|
||||
"230": 617,
|
||||
"52": 334,
|
||||
"691": 550,
|
||||
"373": 259,
|
||||
"377": 212,
|
||||
"976": 428,
|
||||
"382": 297,
|
||||
"1-664": 354,
|
||||
"212": 604,
|
||||
"258": 643,
|
||||
"95": 414,
|
||||
"264": 649,
|
||||
"674": 536,
|
||||
"977": 429,
|
||||
"31": 204,
|
||||
"687": 546,
|
||||
"64": 530,
|
||||
"505": 710,
|
||||
"227": 614,
|
||||
"234": 621,
|
||||
"683": 555,
|
||||
"1-670": 534,
|
||||
"47": 242,
|
||||
"968": 226,
|
||||
"92": 410,
|
||||
"680": 552,
|
||||
"970": 423,
|
||||
"507": 714,
|
||||
"675": 537,
|
||||
"595": 744,
|
||||
"51": 716,
|
||||
"63": 515,
|
||||
"48": 260,
|
||||
"351": 268,
|
||||
"1-787, 1-939": 330,
|
||||
"974": 427,
|
||||
"242": 630,
|
||||
"40": 226,
|
||||
"7": 250,
|
||||
"250": 635,
|
||||
"290": 658,
|
||||
"1-869": 356,
|
||||
"1-758": 358,
|
||||
"508": 308,
|
||||
"1-784": 360,
|
||||
"685": 544,
|
||||
"378": 292,
|
||||
"239": 626,
|
||||
"966": 420,
|
||||
"221": 608,
|
||||
"381": 220,
|
||||
"248": 633,
|
||||
"232": 619,
|
||||
"65": 525,
|
||||
"386": 293,
|
||||
"677": 540,
|
||||
"27": 655,
|
||||
"211": 659,
|
||||
"34": 214,
|
||||
"94": 413,
|
||||
"249": 634,
|
||||
"597": 746,
|
||||
"268": 653,
|
||||
"46": 240,
|
||||
"41": 228,
|
||||
"963": 417,
|
||||
"886": 466,
|
||||
"992": 436,
|
||||
"255": 640,
|
||||
"66": 520,
|
||||
"228": 615,
|
||||
"690": 554,
|
||||
"676": 539,
|
||||
"1-868": 374,
|
||||
"216": 605,
|
||||
"90": 286,
|
||||
"993": 438,
|
||||
"1-649": 376,
|
||||
"688": 553,
|
||||
"1-340": 332,
|
||||
"256": 641,
|
||||
"380": 255,
|
||||
"971": 424,
|
||||
"44": 234,
|
||||
"1": 310,
|
||||
"598": 748,
|
||||
"998": 434,
|
||||
"678": 541,
|
||||
"379": 225,
|
||||
"58": 734,
|
||||
"681": 543,
|
||||
"967": 421,
|
||||
"260": 645,
|
||||
"263": 648,
|
||||
"670": 514,
|
||||
"245": 632,
|
||||
"856": 457,
|
||||
"599": 362,
|
||||
"850": 467,
|
||||
"262": 647,
|
||||
"82": 450,
|
||||
"84": 452
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||
import { UserFacingSocketConfig } from '../Types'
|
||||
import { makeBusinessSocket as _makeSocket } from './business'
|
||||
import { makeRegistrationSocket as _makeSocket } from './registration'
|
||||
|
||||
// export the last socket layer
|
||||
const makeWASocket = (config: UserFacingSocketConfig) => (
|
||||
|
||||
47
src/Socket/mobile-socket.ts
Normal file
47
src/Socket/mobile-socket.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Socket } from 'net'
|
||||
import { MOBILE_ENDPOINT, MOBILE_PORT } from '../Defaults'
|
||||
import { SocketConfig } from '../Types'
|
||||
|
||||
export class MobileSocket extends Socket {
|
||||
constructor(public config: SocketConfig) {
|
||||
super()
|
||||
|
||||
if(config.auth.creds.registered) {
|
||||
this.connect()
|
||||
}
|
||||
|
||||
this.on('data', (d) => {
|
||||
this.emit('message', d)
|
||||
})
|
||||
}
|
||||
|
||||
override connect() {
|
||||
return super.connect(MOBILE_PORT, MOBILE_ENDPOINT, () => {
|
||||
this.emit('open')
|
||||
})
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.readyState === 'open'
|
||||
}
|
||||
|
||||
get isClosed(): boolean {
|
||||
return this.readyState === 'closed'
|
||||
}
|
||||
|
||||
get isClosing(): boolean {
|
||||
return this.isClosed
|
||||
}
|
||||
|
||||
get isConnecting(): boolean {
|
||||
return this.readyState === 'opening'
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.end()
|
||||
}
|
||||
|
||||
send(data: unknown, cb?: ((err?: Error | undefined) => void) | undefined) {
|
||||
return super.write(data as Uint8Array | string, cb as ((err?: Error | undefined) => void))
|
||||
}
|
||||
}
|
||||
251
src/Socket/registration.ts
Normal file
251
src/Socket/registration.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { MOBILE_REGISTRATION_ENDPOINT, MOBILE_TOKEN, MOBILE_USERAGENT, REGISTRATION_PUBLIC_KEY } from '../Defaults'
|
||||
import { KeyPair, SignedKeyPair, SocketConfig } from '../Types'
|
||||
import { aesEncryptGCM, Curve, md5 } from '../Utils/crypto'
|
||||
import { jidEncode } from '../WABinary'
|
||||
import { makeBusinessSocket } from './business'
|
||||
import { MobileSocket } from './mobile-socket'
|
||||
|
||||
function urlencode(str: string) {
|
||||
return str.replace(/-/g, '%2d').replace(/_/g, '%5f').replace(/~/g, '%7e')
|
||||
}
|
||||
|
||||
const validRegistrationOptions = (config: RegistrationOptions) => config?.phoneNumberCountryCode &&
|
||||
config.phoneNumberNationalNumber &&
|
||||
config.phoneNumberMobileCountryCode
|
||||
|
||||
export const makeRegistrationSocket = (config: SocketConfig) => {
|
||||
const sock = makeBusinessSocket(config)
|
||||
|
||||
const register = async(code: string) => {
|
||||
if(!validRegistrationOptions(config.auth.creds.registration)) {
|
||||
throw new Error('please specify the registration options')
|
||||
}
|
||||
|
||||
const result = await mobileRegister({ ...sock.authState.creds, ...sock.authState.creds.registration as RegistrationOptions, code })
|
||||
|
||||
sock.authState.creds.me = {
|
||||
id: jidEncode(result.login!, 's.whatsapp.net'),
|
||||
name: '~'
|
||||
}
|
||||
|
||||
sock.authState.creds.registered = true
|
||||
sock.ev.emit('creds.update', sock.authState.creds)
|
||||
|
||||
if(sock.ws instanceof MobileSocket) {
|
||||
sock.ws.connect()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const requestRegistrationCode = async(registrationOptions?: RegistrationOptions) => {
|
||||
registrationOptions = registrationOptions || config.auth.creds.registration
|
||||
if(!validRegistrationOptions(registrationOptions)) {
|
||||
throw new Error('Invalid registration options')
|
||||
}
|
||||
|
||||
sock.authState.creds.registration = registrationOptions
|
||||
|
||||
sock.ev.emit('creds.update', sock.authState.creds)
|
||||
|
||||
return mobileRegisterCode({ ...config.auth.creds, ...registrationOptions })
|
||||
}
|
||||
|
||||
return {
|
||||
...sock,
|
||||
register,
|
||||
requestRegistrationCode,
|
||||
}
|
||||
}
|
||||
|
||||
// Backup_token: Base64.getEncoder().encodeToString(Arrays.copyOfRange(Base64.getDecoder().decode(UUID.randomUUID().toString().replace('-','')),0,15))
|
||||
|
||||
export interface RegistrationData {
|
||||
registrationId: number
|
||||
signedPreKey: SignedKeyPair
|
||||
noiseKey: KeyPair
|
||||
signedIdentityKey: KeyPair
|
||||
identityId: Buffer
|
||||
phoneId: string
|
||||
deviceId: string
|
||||
backupToken: Buffer
|
||||
}
|
||||
|
||||
export interface RegistrationOptions {
|
||||
/** your phone number */
|
||||
phoneNumber?: string
|
||||
/** the country code of your phone number */
|
||||
phoneNumberCountryCode: string
|
||||
/** your phone number without country code */
|
||||
phoneNumberNationalNumber: string
|
||||
/** the country code of your mobile network
|
||||
* @see {@link https://de.wikipedia.org/wiki/Mobile_Country_Code}
|
||||
*/
|
||||
phoneNumberMobileCountryCode: string
|
||||
/** the network code of your mobile network
|
||||
* @see {@link https://de.wikipedia.org/wiki/Mobile_Network_Code}
|
||||
*/
|
||||
phoneNumberMobileNetworkCode: string
|
||||
/**
|
||||
* How to send the one time code
|
||||
*/
|
||||
method?: 'sms' | 'voice'
|
||||
}
|
||||
|
||||
export type RegistrationParams = RegistrationData & RegistrationOptions
|
||||
|
||||
function convertBufferToUrlHex(buffer: Buffer) {
|
||||
var id = ''
|
||||
|
||||
buffer.forEach((x) => {
|
||||
// encode random identity_id buffer as percentage url encoding
|
||||
id += `%${x.toString(16).padStart(2, '0').toLowerCase()}`
|
||||
})
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
export function registrationParams(params: RegistrationParams) {
|
||||
const e_regid = Buffer.alloc(4)
|
||||
e_regid.writeInt32BE(params.registrationId)
|
||||
|
||||
const e_skey_id = Buffer.alloc(3)
|
||||
e_skey_id.writeInt16BE(params.signedPreKey.keyId)
|
||||
|
||||
params.phoneNumberCountryCode = params.phoneNumberCountryCode.replace('+', '').trim()
|
||||
params.phoneNumberNationalNumber = params.phoneNumberNationalNumber.replace(/[/-\s)(]/g, '').trim()
|
||||
|
||||
return {
|
||||
cc: params.phoneNumberCountryCode,
|
||||
in: params.phoneNumberNationalNumber,
|
||||
Rc: '0',
|
||||
lg: 'en',
|
||||
lc: 'GB',
|
||||
mistyped: '6',
|
||||
authkey: Buffer.from(params.noiseKey.public).toString('base64url'),
|
||||
e_regid: e_regid.toString('base64url'),
|
||||
e_keytype: 'BQ',
|
||||
e_ident: Buffer.from(params.signedIdentityKey.public).toString('base64url'),
|
||||
// e_skey_id: e_skey_id.toString('base64url'),
|
||||
e_skey_id: 'AAAA',
|
||||
e_skey_val: Buffer.from(params.signedPreKey.keyPair.public).toString('base64url'),
|
||||
e_skey_sig: Buffer.from(params.signedPreKey.signature).toString('base64url'),
|
||||
fdid: params.phoneId,
|
||||
network_ratio_type: '1',
|
||||
expid: params.deviceId,
|
||||
simnum: '1',
|
||||
hasinrc: '1',
|
||||
pid: Math.floor(Math.random() * 1000).toString(),
|
||||
id: convertBufferToUrlHex(params.identityId),
|
||||
backup_token: convertBufferToUrlHex(params.backupToken),
|
||||
token: md5(Buffer.concat([MOBILE_TOKEN, Buffer.from(params.phoneNumberNationalNumber)])).toString('hex'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a registration code for the given phone number.
|
||||
*/
|
||||
export function mobileRegisterCode(params: RegistrationParams) {
|
||||
return mobileRegisterFetch('/code', {
|
||||
params: {
|
||||
...registrationParams(params),
|
||||
mcc: `${params.phoneNumberMobileCountryCode}`.padStart(3, '0'),
|
||||
mnc: `${params.phoneNumberMobileNetworkCode || '001'}`.padStart(3, '0'),
|
||||
sim_mcc: '000',
|
||||
sim_mnc: '000',
|
||||
method: params?.method || 'sms',
|
||||
reason: '',
|
||||
hasav: '1'
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function mobileRegisterExists(params: RegistrationParams) {
|
||||
return mobileRegisterFetch('/exist', {
|
||||
params: registrationParams(params)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the phone number on whatsapp with the received OTP code.
|
||||
*/
|
||||
export async function mobileRegister(params: RegistrationParams & { code: string }) {
|
||||
//const result = await mobileRegisterFetch(`/reg_onboard_abprop?cc=${params.phoneNumberCountryCode}&in=${params.phoneNumberNationalNumber}&rc=0`)
|
||||
|
||||
return mobileRegisterFetch('/register', {
|
||||
params: { ...registrationParams(params), code: params.code.replace('-', '') },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given string as AEAD aes-256-gcm with the public whatsapp key and a random keypair.
|
||||
*/
|
||||
export function mobileRegisterEncrypt(data: string) {
|
||||
const keypair = Curve.generateKeyPair()
|
||||
const key = Curve.sharedKey(keypair.private, REGISTRATION_PUBLIC_KEY)
|
||||
|
||||
const buffer = aesEncryptGCM(Buffer.from(data), new Uint8Array(key), Buffer.alloc(12), Buffer.alloc(0))
|
||||
|
||||
return Buffer.concat([Buffer.from(keypair.public), buffer]).toString('base64url')
|
||||
}
|
||||
|
||||
export async function mobileRegisterFetch(path: string, opts: { params?: Record<string, string>, headers?: Record<string, string> } = {}) {
|
||||
let url = `${MOBILE_REGISTRATION_ENDPOINT}${path}`
|
||||
|
||||
if(opts.params) {
|
||||
const parameter = [] as string[]
|
||||
|
||||
for(const param in opts.params) {
|
||||
parameter.push(param + '=' + urlencode(opts.params[param]))
|
||||
}
|
||||
|
||||
console.log('parameter', opts.params, parameter)
|
||||
|
||||
// const params = urlencode(mobileRegisterEncrypt(parameter.join('&')))
|
||||
// url += `?ENC=${params}`
|
||||
url += `?${parameter.join('&')}`
|
||||
}
|
||||
|
||||
if(!opts.headers) {
|
||||
opts.headers = {}
|
||||
}
|
||||
|
||||
opts.headers['User-Agent'] = MOBILE_USERAGENT
|
||||
|
||||
const response = await fetch(url, opts)
|
||||
|
||||
const text = await response.text()
|
||||
|
||||
try {
|
||||
var json = JSON.parse(text)
|
||||
} catch(error) {
|
||||
throw text
|
||||
}
|
||||
|
||||
if(!response.ok || json.reason) {
|
||||
throw json
|
||||
}
|
||||
|
||||
if(json.status && !['ok', 'sent'].includes(json.status)) {
|
||||
throw json
|
||||
}
|
||||
|
||||
return json as ExistsResponse
|
||||
}
|
||||
|
||||
|
||||
export interface ExistsResponse {
|
||||
status: 'fail'
|
||||
voice_length?: number
|
||||
voice_wait?: number
|
||||
sms_length?: number
|
||||
sms_wait?: number
|
||||
reason?: 'incorrect' | 'missing_param'
|
||||
login?: string
|
||||
flash_type?: number
|
||||
ab_hash?: string
|
||||
ab_key?: string
|
||||
exp_cfg?: string
|
||||
lid?: string
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { promisify } from 'util'
|
||||
import WebSocket from 'ws'
|
||||
import { proto } from '../../WAProto'
|
||||
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT } from '../Defaults'
|
||||
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MOBILE_NOISE_HEADER, NOISE_WA_HEADER } from '../Defaults'
|
||||
import { DisconnectReason, SocketConfig } from '../Types'
|
||||
import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils'
|
||||
import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateMobileNode, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils'
|
||||
import { makeEventBuffer } from '../Utils/event-buffer'
|
||||
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary'
|
||||
import { assertNodeErrorFree, BinaryNode, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary'
|
||||
import { MobileSocket } from './mobile-socket'
|
||||
import { WebSocket } from './web-socket'
|
||||
|
||||
/**
|
||||
* Connects to WA servers and performs:
|
||||
@@ -14,37 +16,35 @@ import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild,
|
||||
* - listen to messages and emit events
|
||||
* - query phone connection
|
||||
*/
|
||||
export const makeSocket = ({
|
||||
waWebSocketUrl,
|
||||
connectTimeoutMs,
|
||||
logger,
|
||||
agent,
|
||||
keepAliveIntervalMs,
|
||||
version,
|
||||
browser,
|
||||
auth: authState,
|
||||
printQRInTerminal,
|
||||
defaultQueryTimeoutMs,
|
||||
syncFullHistory,
|
||||
transactionOpts,
|
||||
qrTimeout,
|
||||
options,
|
||||
makeSignalRepository
|
||||
}: SocketConfig) => {
|
||||
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
||||
origin: DEFAULT_ORIGIN,
|
||||
headers: options.headers as {},
|
||||
handshakeTimeout: connectTimeoutMs,
|
||||
timeout: connectTimeoutMs,
|
||||
agent
|
||||
})
|
||||
|
||||
export const makeSocket = (config: SocketConfig) => {
|
||||
const {
|
||||
connectTimeoutMs,
|
||||
logger,
|
||||
keepAliveIntervalMs,
|
||||
browser,
|
||||
auth: authState,
|
||||
printQRInTerminal,
|
||||
defaultQueryTimeoutMs,
|
||||
transactionOpts,
|
||||
qrTimeout,
|
||||
makeSignalRepository,
|
||||
} = config
|
||||
|
||||
config.mobile = config.mobile || config.auth.creds.registered
|
||||
const ws = config.mobile ? new MobileSocket(config) : new WebSocket(config)
|
||||
ws.setMaxListeners(0)
|
||||
|
||||
const ev = makeEventBuffer(logger)
|
||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||
const ephemeralKeyPair = Curve.generateKeyPair()
|
||||
/** WA noise protocol wrapper */
|
||||
const noise = makeNoiseHandler(ephemeralKeyPair, logger)
|
||||
const noise = makeNoiseHandler({
|
||||
keyPair: ephemeralKeyPair,
|
||||
NOISE_HEADER: config.mobile ? MOBILE_NOISE_HEADER : NOISE_WA_HEADER,
|
||||
mobile: config.mobile,
|
||||
logger
|
||||
})
|
||||
|
||||
const { creds } = authState
|
||||
// add transaction capability
|
||||
@@ -63,7 +63,7 @@ export const makeSocket = ({
|
||||
const sendPromise = promisify<void>(ws.send)
|
||||
/** send a raw buffer */
|
||||
const sendRawMessage = async(data: Uint8Array | Buffer) => {
|
||||
if(ws.readyState !== ws.OPEN) {
|
||||
if(!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const makeSocket = ({
|
||||
/** send a binary node */
|
||||
const sendNode = (frame: BinaryNode) => {
|
||||
if(logger.level === 'trace') {
|
||||
logger.trace({ msgId: frame.attrs.id, fromMe: true, frame }, 'communication')
|
||||
logger.trace(binaryNodeToString(frame), 'xml send')
|
||||
}
|
||||
|
||||
const buff = encodeBinaryNode(frame)
|
||||
@@ -101,7 +101,7 @@ export const makeSocket = ({
|
||||
|
||||
/** await the next incoming message */
|
||||
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
|
||||
if(ws.readyState !== ws.OPEN) {
|
||||
if(!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', {
|
||||
statusCode: DisconnectReason.connectionClosed
|
||||
})
|
||||
@@ -186,21 +186,21 @@ export const makeSocket = ({
|
||||
}
|
||||
helloMsg = proto.HandshakeMessage.fromObject(helloMsg)
|
||||
|
||||
logger.info({ browser, helloMsg }, 'connected to WA Web')
|
||||
logger.info({ browser, helloMsg }, 'connected to WA')
|
||||
|
||||
const init = proto.HandshakeMessage.encode(helloMsg).finish()
|
||||
|
||||
const result = await awaitNextMessage<Uint8Array>(init)
|
||||
const handshake = proto.HandshakeMessage.decode(result)
|
||||
|
||||
logger.trace({ handshake }, 'handshake recv from WA Web')
|
||||
logger.trace({ handshake }, 'handshake recv from WA')
|
||||
|
||||
const keyEnc = noise.processHandshake(handshake, creds.noiseKey)
|
||||
|
||||
const config = { version, browser, syncFullHistory }
|
||||
|
||||
let node: proto.IClientPayload
|
||||
if(!creds.me) {
|
||||
if(config.mobile) {
|
||||
node = generateMobileNode(config)
|
||||
} else if(!creds.me) {
|
||||
node = generateRegistrationNode(creds, config)
|
||||
logger.info({ node }, 'not logged in, attempting registration...')
|
||||
} else {
|
||||
@@ -276,7 +276,7 @@ export const makeSocket = ({
|
||||
const msgId = frame.attrs.id
|
||||
|
||||
if(logger.level === 'trace') {
|
||||
logger.trace({ msgId, fromMe: false, frame }, 'communication')
|
||||
logger.trace(binaryNodeToString(frame), 'recv xml')
|
||||
}
|
||||
|
||||
/* Check if this is a response to a message we sent */
|
||||
@@ -321,7 +321,7 @@ export const makeSocket = ({
|
||||
ws.removeAllListeners('open')
|
||||
ws.removeAllListeners('message')
|
||||
|
||||
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
|
||||
if(!ws.isClosed && !ws.isClosing) {
|
||||
try {
|
||||
ws.close()
|
||||
} catch{ }
|
||||
@@ -338,11 +338,11 @@ export const makeSocket = ({
|
||||
}
|
||||
|
||||
const waitForSocketOpen = async() => {
|
||||
if(ws.readyState === ws.OPEN) {
|
||||
if(ws.isOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
||||
if(ws.isClosed || ws.isClosing) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ export const makeSocket = ({
|
||||
*/
|
||||
if(diff > keepAliveIntervalMs + 5000) {
|
||||
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
||||
} else if(ws.readyState === ws.OPEN) {
|
||||
} else if(ws.isOpen) {
|
||||
// if its all good, send a keep alive request
|
||||
query(
|
||||
{
|
||||
@@ -472,7 +472,7 @@ export const makeSocket = ({
|
||||
|
||||
let qrMs = qrTimeout || 60_000 // time to let a QR live
|
||||
const genPairQR = () => {
|
||||
if(ws.readyState !== ws.OPEN) {
|
||||
if(!ws.isOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
31
src/Socket/web-socket.ts
Normal file
31
src/Socket/web-socket.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { WebSocket as WS } from 'ws'
|
||||
import { DEFAULT_ORIGIN } from '../Defaults'
|
||||
import { SocketConfig } from '../Types'
|
||||
|
||||
export class WebSocket extends WS {
|
||||
constructor(public config: SocketConfig) {
|
||||
super(config.waWebSocketUrl, undefined, {
|
||||
origin: DEFAULT_ORIGIN,
|
||||
headers: config.options.headers as Record<string, string>,
|
||||
handshakeTimeout: config.connectTimeoutMs,
|
||||
timeout: config.connectTimeoutMs,
|
||||
agent: config.agent,
|
||||
})
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return this.readyState === WS.OPEN
|
||||
}
|
||||
|
||||
get isClosed() {
|
||||
return this.readyState === WS.CLOSED
|
||||
}
|
||||
|
||||
get isClosing() {
|
||||
return this.readyState === WS.CLOSING
|
||||
}
|
||||
|
||||
get isConnecting() {
|
||||
return this.readyState === WS.CONNECTING
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { proto } from '../../WAProto'
|
||||
import { RegistrationOptions } from '../Socket/registration'
|
||||
import type { Contact } from './Contact'
|
||||
import type { MinimalMessage } from './Message'
|
||||
|
||||
@@ -58,6 +59,13 @@ export type AuthenticationCreds = SignalCreds & {
|
||||
/** number of times history & app state has been synced */
|
||||
accountSyncCounter: number
|
||||
accountSettings: AccountSettings
|
||||
// mobile creds
|
||||
deviceId: string
|
||||
phoneId: string
|
||||
identityId: Buffer
|
||||
registered: boolean
|
||||
backupToken: Buffer
|
||||
registration: RegistrationOptions
|
||||
}
|
||||
|
||||
export type SignalDataTypeMap = {
|
||||
|
||||
@@ -31,6 +31,8 @@ export type SocketConfig = {
|
||||
defaultQueryTimeoutMs: number | undefined
|
||||
/** ping-pong interval for WS connection */
|
||||
keepAliveIntervalMs: number
|
||||
/** should baileys use the mobile api instead of the multi device api */
|
||||
mobile?: boolean
|
||||
/** proxy agent */
|
||||
agent?: Agent
|
||||
/** pino logger */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import NodeCache from 'node-cache'
|
||||
import type { Logger } from 'pino'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { DEFAULT_CACHE_TTLS } from '../Defaults'
|
||||
import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
|
||||
import { Curve, signedKeyPair } from './crypto'
|
||||
@@ -202,6 +203,13 @@ export const initAuthCreds = (): AuthenticationCreds => {
|
||||
accountSyncCounter: 0,
|
||||
accountSettings: {
|
||||
unarchiveChats: false
|
||||
}
|
||||
},
|
||||
// mobile creds
|
||||
deviceId: Buffer.from(uuidv4().replace(/-/g, ''), 'hex').toString('base64url'),
|
||||
phoneId: uuidv4(),
|
||||
identityId: randomBytes(20),
|
||||
registered: false,
|
||||
backupToken: randomBytes(20),
|
||||
registration: {} as RegistrationOptions
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,10 @@ export function sha256(buffer: Buffer) {
|
||||
return createHash('sha256').update(buffer).digest()
|
||||
}
|
||||
|
||||
export function md5(buffer: Buffer) {
|
||||
return createHash('md5').update(buffer).digest()
|
||||
}
|
||||
|
||||
// HKDF key expansion
|
||||
export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) {
|
||||
return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { Logger } from 'pino'
|
||||
import { proto } from '../../WAProto'
|
||||
import { NOISE_MODE, NOISE_WA_HEADER, WA_CERT_DETAILS } from '../Defaults'
|
||||
import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults'
|
||||
import { KeyPair } from '../Types'
|
||||
import { BinaryNode, decodeBinaryNode } from '../WABinary'
|
||||
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto'
|
||||
@@ -13,10 +13,17 @@ const generateIV = (counter: number) => {
|
||||
return new Uint8Array(iv)
|
||||
}
|
||||
|
||||
export const makeNoiseHandler = (
|
||||
{ public: publicKey, private: privateKey }: KeyPair,
|
||||
export const makeNoiseHandler = ({
|
||||
keyPair: { private: privateKey, public: publicKey },
|
||||
NOISE_HEADER,
|
||||
mobile,
|
||||
logger,
|
||||
}: {
|
||||
keyPair: KeyPair
|
||||
NOISE_HEADER: Uint8Array
|
||||
mobile: boolean
|
||||
logger: Logger
|
||||
) => {
|
||||
}) => {
|
||||
logger = logger.child({ class: 'ns' })
|
||||
|
||||
const authenticate = (data: Uint8Array) => {
|
||||
@@ -86,7 +93,7 @@ export const makeNoiseHandler = (
|
||||
|
||||
let inBytes = Buffer.alloc(0)
|
||||
|
||||
authenticate(NOISE_WA_HEADER)
|
||||
authenticate(NOISE_HEADER)
|
||||
authenticate(publicKey)
|
||||
|
||||
return {
|
||||
@@ -103,12 +110,18 @@ export const makeNoiseHandler = (
|
||||
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
||||
|
||||
const certDecoded = decrypt(serverHello!.payload!)
|
||||
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
|
||||
|
||||
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
|
||||
if(mobile) {
|
||||
const cert = proto.CertChain.NoiseCertificate.decode(certDecoded)
|
||||
logger.debug(cert)
|
||||
} else {
|
||||
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
|
||||
|
||||
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
|
||||
throw new Boom('certification match failed', { statusCode: 400 })
|
||||
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
|
||||
|
||||
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
|
||||
throw new Boom('certification match failed', { statusCode: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const keyEnc = encrypt(noiseKey.public)
|
||||
@@ -121,11 +134,11 @@ export const makeNoiseHandler = (
|
||||
data = encrypt(data)
|
||||
}
|
||||
|
||||
const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length
|
||||
const introSize = sentIntro ? 0 : NOISE_HEADER.length
|
||||
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
|
||||
|
||||
if(!sentIntro) {
|
||||
frame.set(NOISE_WA_HEADER)
|
||||
frame.set(NOISE_HEADER)
|
||||
sentIntro = true
|
||||
}
|
||||
|
||||
|
||||
@@ -8,26 +8,31 @@ import { Curve, hmacSign } from './crypto'
|
||||
import { encodeBigEndian } from './generics'
|
||||
import { createSignalIdentity } from './signal'
|
||||
|
||||
type ClientPayloadConfig = Pick<SocketConfig, 'version' | 'browser' | 'syncFullHistory'>
|
||||
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
||||
const osVersion = config.mobile ? '15.3.1' : '0.1'
|
||||
const version = config.mobile ? [2, 22, 24] : config.version
|
||||
const device = config.mobile ? 'iPhone_7' : 'Desktop'
|
||||
const manufacturer = config.mobile ? 'Apple' : ''
|
||||
const platform = config.mobile ? proto.ClientPayload.UserAgent.Platform.IOS : proto.ClientPayload.UserAgent.Platform.WEB
|
||||
const phoneId = config.mobile ? { phoneId: config.auth.creds.phoneId } : {}
|
||||
|
||||
const getUserAgent = ({ version }: ClientPayloadConfig): proto.ClientPayload.IUserAgent => {
|
||||
const osVersion = '0.1'
|
||||
return {
|
||||
appVersion: {
|
||||
primary: version[0],
|
||||
secondary: version[1],
|
||||
tertiary: version[2],
|
||||
},
|
||||
platform: proto.ClientPayload.UserAgent.Platform.WEB,
|
||||
platform,
|
||||
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
||||
mcc: '000',
|
||||
mnc: '000',
|
||||
mcc: config.auth.creds.registration?.phoneNumberMobileCountryCode || '000',
|
||||
mnc: config.auth.creds.registration?.phoneNumberMobileNetworkCode || '000',
|
||||
osVersion: osVersion,
|
||||
manufacturer: '',
|
||||
device: 'Desktop',
|
||||
manufacturer,
|
||||
device,
|
||||
osBuildNumber: osVersion,
|
||||
localeLanguageIso6391: 'en',
|
||||
localeCountryIso31661Alpha2: 'US',
|
||||
...phoneId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +41,7 @@ const PLATFORM_MAP = {
|
||||
'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN32
|
||||
}
|
||||
|
||||
const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo => {
|
||||
const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => {
|
||||
let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER
|
||||
if(config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) {
|
||||
webSubPlatform = PLATFORM_MAP[config.browser[0]]
|
||||
@@ -45,16 +50,43 @@ const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo =
|
||||
return { webSubPlatform }
|
||||
}
|
||||
|
||||
const getClientPayload = (config: ClientPayloadConfig): proto.IClientPayload => {
|
||||
return {
|
||||
const getClientPayload = (config: SocketConfig) => {
|
||||
const payload: proto.IClientPayload = {
|
||||
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
|
||||
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
|
||||
userAgent: getUserAgent(config),
|
||||
webInfo: getWebInfo(config),
|
||||
}
|
||||
|
||||
if(!config.mobile) {
|
||||
payload.webInfo = getWebInfo(config)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export const generateLoginNode = (userJid: string, config: ClientPayloadConfig): proto.IClientPayload => {
|
||||
export const generateMobileNode = (config: SocketConfig): proto.IClientPayload => {
|
||||
if(!config.auth.creds) {
|
||||
throw new Boom('No registration data found', { data: config })
|
||||
}
|
||||
|
||||
const payload: proto.IClientPayload = {
|
||||
...getClientPayload(config),
|
||||
sessionId: Math.floor(Math.random() * 999999999 + 1),
|
||||
shortConnect: true,
|
||||
connectAttemptCount: 0,
|
||||
device: 0,
|
||||
dnsSource: {
|
||||
appCached: false,
|
||||
dnsMethod: proto.ClientPayload.DNSSource.DNSResolutionMethod.SYSTEM,
|
||||
},
|
||||
passive: false, // XMPP heartbeat setting (false: server actively pings) (true: client actively pings)
|
||||
pushName: 'test',
|
||||
username: Number(`${config.auth.creds.registration.phoneNumberCountryCode}${config.auth.creds.registration.phoneNumberNationalNumber}`),
|
||||
}
|
||||
return proto.ClientPayload.fromObject(payload)
|
||||
}
|
||||
|
||||
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
|
||||
const { user, device } = jidDecode(userJid)!
|
||||
const payload: proto.IClientPayload = {
|
||||
...getClientPayload(config),
|
||||
@@ -67,7 +99,7 @@ export const generateLoginNode = (userJid: string, config: ClientPayloadConfig):
|
||||
|
||||
export const generateRegistrationNode = (
|
||||
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
|
||||
config: ClientPayloadConfig
|
||||
config: SocketConfig
|
||||
) => {
|
||||
// the app version needs to be md5 hashed
|
||||
// and passed in
|
||||
|
||||
@@ -87,4 +87,35 @@ function bufferToUInt(e: Uint8Array | Buffer, t: number) {
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
const tabs = (n: number) => '\t'.repeat(n)
|
||||
|
||||
export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i = 0) {
|
||||
if(!node) {
|
||||
return node
|
||||
}
|
||||
|
||||
if(typeof node === 'string') {
|
||||
return tabs(i) + node
|
||||
}
|
||||
|
||||
if(node instanceof Uint8Array) {
|
||||
return tabs(i) + Buffer.from(node).toString('hex')
|
||||
}
|
||||
|
||||
if(Array.isArray(node)) {
|
||||
return node.map((x) => tabs(i + 1) + binaryNodeToString(x, i + 1)).join('\n')
|
||||
}
|
||||
|
||||
const children = binaryNodeToString(node.content, i + 1)
|
||||
|
||||
const tag = `<${node.tag} ${Object.entries(node.attrs || {})
|
||||
.filter(([, v]) => v !== undefined)
|
||||
.map(([k, v]) => `${k}='${v}'`)
|
||||
.join(' ')}`
|
||||
|
||||
const content: string = children ? `>\n${children}\n${tabs(i)}</${node.tag}>` : '/>'
|
||||
|
||||
return tag + content
|
||||
}
|
||||
Reference in New Issue
Block a user