From 87ff6ba67d4df0b4c0ae3db86220b816ec6a1e75 Mon Sep 17 00:00:00 2001 From: George Hazan Date: Sat, 1 Oct 2022 14:20:05 +0300 Subject: WhatsApp: - first version that registers ok in the phone (YAHOO!!); - the rest of legacy code removed; - class WAJid introduced to handle various perversions; - ServerThread behavior changed to use global variables instead of locals; - IQ processing core extracted to the separate module; - added processing --- protocols/WhatsAppWeb/src/avatars.cpp | 4 +- protocols/WhatsAppWeb/src/chats.cpp | 2 +- protocols/WhatsAppWeb/src/db.h | 10 +- protocols/WhatsAppWeb/src/dicts.h | 7 + protocols/WhatsAppWeb/src/iq.cpp | 308 ++++++++++++++++++ protocols/WhatsAppWeb/src/main.cpp | 2 +- protocols/WhatsAppWeb/src/noise.cpp | 3 - protocols/WhatsAppWeb/src/proto.cpp | 21 +- protocols/WhatsAppWeb/src/proto.h | 15 +- protocols/WhatsAppWeb/src/server.cpp | 469 +++++---------------------- protocols/WhatsAppWeb/src/stdafx.cxx | 2 +- protocols/WhatsAppWeb/src/utils.cpp | 51 ++- protocols/WhatsAppWeb/src/utils.h | 13 + protocols/WhatsAppWeb/src/wanode.cpp | 570 +++++++++++++++++++++++++++++++++ protocols/WhatsAppWeb/src/wareader.cpp | 570 --------------------------------- 15 files changed, 1038 insertions(+), 1009 deletions(-) create mode 100644 protocols/WhatsAppWeb/src/iq.cpp create mode 100644 protocols/WhatsAppWeb/src/wanode.cpp delete mode 100644 protocols/WhatsAppWeb/src/wareader.cpp (limited to 'protocols/WhatsAppWeb/src') diff --git a/protocols/WhatsAppWeb/src/avatars.cpp b/protocols/WhatsAppWeb/src/avatars.cpp index 5649067330..c9eefb9d6b 100644 --- a/protocols/WhatsAppWeb/src/avatars.cpp +++ b/protocols/WhatsAppWeb/src/avatars.cpp @@ -44,7 +44,7 @@ INT_PTR WhatsAppProto::GetAvatarInfo(WPARAM wParam, LPARAM lParam) { PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION*)lParam; - ptrA id(getStringA(pai->hContact, isChatRoom(pai->hContact) ? "ChatRoomID" : DBKEY_ID)); + ptrA id(getStringA(pai->hContact, isChatRoom(pai->hContact) ? "ChatRoomID" : DBKEY_JID)); if (id == NULL) return GAIR_NOAVATAR; @@ -92,7 +92,7 @@ CMStringW WhatsAppProto::GetAvatarFileName(MCONTACT hContact) CMStringA jid; if (hContact != NULL) { - ptrA szId(getStringA(hContact, isChatRoom(hContact) ? "ChatRoomID" : DBKEY_ID)); + ptrA szId(getStringA(hContact, isChatRoom(hContact) ? "ChatRoomID" : DBKEY_JID)); if (szId == NULL) return L""; diff --git a/protocols/WhatsAppWeb/src/chats.cpp b/protocols/WhatsAppWeb/src/chats.cpp index 3017313a5a..a478fae692 100644 --- a/protocols/WhatsAppWeb/src/chats.cpp +++ b/protocols/WhatsAppWeb/src/chats.cpp @@ -1,7 +1,7 @@ /* WhatsAppWeb plugin for Miranda NG -Copyright © 2019 George Hazan +Copyright © 2019-22 George Hazan */ diff --git a/protocols/WhatsAppWeb/src/db.h b/protocols/WhatsAppWeb/src/db.h index 66f1686ecc..adab0945e7 100644 --- a/protocols/WhatsAppWeb/src/db.h +++ b/protocols/WhatsAppWeb/src/db.h @@ -8,14 +8,8 @@ Copyright #define MODULENAME "WhatsApp" // DB keys -#define DBKEY_ID "ID" -#define DBKEY_LOGIN "Login" -#define DBKEY_CC "CountryCode" - -#define DBKEY_CLIENT_ID "ClientId" -#define DBKEY_CLIENT_TOKEN "ClientToken" -#define DBKEY_SERVER_TOKEN "ServerToken" -#define DBKEY_BROWSER_TOKEN "BrowserToken" +#define DBKEY_JID "jid" +#define DBKEY_DEVICE_ID "DeviceId" #define DBKEY_NOISE_PUB "NoisePublicKey" #define DBKEY_NOISE_PRIV "NoisePrivateKey" diff --git a/protocols/WhatsAppWeb/src/dicts.h b/protocols/WhatsAppWeb/src/dicts.h index a8c8b25d61..281983c618 100644 --- a/protocols/WhatsAppWeb/src/dicts.h +++ b/protocols/WhatsAppWeb/src/dicts.h @@ -1,3 +1,10 @@ +/* + +WhatsAppWeb plugin for Miranda NG +Copyright © 2019-22 George Hazan + +*/ + static char *SingleByteTokens[] = { "", "xmlstreamstart", "xmlstreamend", "s.whatsapp.net", "type", "participant", "from", "receipt", "id", "broadcast", "status", "message", "notification", "notify", "to", "jid", "user", "class", "offline", "g.us", "result", "mediatype", "enc", "skmsg", "off_cnt", "xmlns", "presence", "participants", "ack", "t", "iq", "device_hash", "read", "value", "media", "picture", diff --git a/protocols/WhatsAppWeb/src/iq.cpp b/protocols/WhatsAppWeb/src/iq.cpp new file mode 100644 index 0000000000..1442e5c02f --- /dev/null +++ b/protocols/WhatsAppWeb/src/iq.cpp @@ -0,0 +1,308 @@ +/* + +WhatsAppWeb plugin for Miranda NG +Copyright © 2019-22 George Hazan + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnStreamError(const WANode &node) +{ + m_bTerminated = true; + + if (auto *pszCode = node.getAttr("code")) { + switch (atoi(pszCode)) { + case 401: + debugLogA("Connection logged out from another device, exiting"); + break; + + case 408: + debugLogA("Connection lost, exiting"); + break; + + case 411: + debugLogA("Conflict between two devices, exiting"); + break; + + case 428: + debugLogA("Connection forcibly closed by the server, exiting"); + break; + + case 440: + debugLogA("Connection replaced from another device, exiting"); + break; + + case 515: + debugLogA("Server required to restart immediately, leaving thread"); + m_bRespawn = true; + break; + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqPairDevice(const WANode &node) +{ + WANode reply("iq"); + reply << CHAR_PARAM("to", S_WHATSAPP_NET) << CHAR_PARAM("type", "result") << CHAR_PARAM("id", node.getAttr("id")); + WSSendNode(reply); + + if (auto *pRef = node.getChild("pair-device")->getChild("ref")) { + ShowQrCode(pRef->getBody()); + } + else { + debugLogA("OnIqPairDevice: got reply without ref, exiting"); + ShutdownSession(); + } +} + +void WhatsAppProto::OnIqPairSuccess(const WANode &node) +{ + CloseQrDialog(); + + auto *pRoot = node.getChild("pair-success"); + + try { + if (auto *pPlatform = pRoot->getChild("platform")) + debugLogA("Got response from platform: %s", pPlatform->getBody().c_str()); + + if (auto *pBiz = pRoot->getChild("biz")) + if (auto *pszName = pBiz->getAttr("name")) + setUString("Nick", pszName); + + if (auto *pDevice = pRoot->getChild("device")) { + if (auto *pszJid = pDevice->getAttr("jid")) { + WAJid jid(pszJid); + m_szJid = jid.user + "@" + jid.server; + setUString(DBKEY_JID, m_szJid); + setDword(DBKEY_DEVICE_ID, jid.device); + } + } + else throw "OnIqPairSuccess: got reply without device info, exiting"; + + if (auto *pIdentity = pRoot->getChild("device-identity")) { + proto::ADVSignedDeviceIdentityHMAC payload; + if (!payload.ParseFromArray(pIdentity->content.data(), (int)pIdentity->content.length())) + throw "OnIqPairSuccess: got reply with invalid identity, exiting"; + + auto &hmac = payload.hmac(); + auto &details = payload.details(); + { + // check details signature using HMAC + uint8_t signature[32]; + unsigned int out_len = sizeof(signature); + MBinBuffer secret(getBlob(DBKEY_SECRET_KEY)); + HMAC(EVP_sha256(), secret.data(), (int)secret.length(), (BYTE *)details.c_str(), (int)details.size(), signature, &out_len); + if (memcmp(hmac.c_str(), signature, sizeof(signature))) + throw "OnIqPairSuccess: got reply with invalid details signature, exiting"; + } + + proto::ADVSignedDeviceIdentity account; + if (!account.ParseFromString(details)) + throw "OnIqPairSuccess: got reply with invalid account, exiting"; + + auto &deviceDetails = account.details(); + auto &accountSignature = account.accountsignature(); + auto &accountSignatureKey = account.accountsignaturekey(); + { + MBinBuffer buf; + buf.append("\x06\x00", 2); + buf.append(deviceDetails.c_str(), deviceDetails.size()); + buf.append(m_noise->signedIdentity.pub.data(), m_noise->signedIdentity.pub.length()); + + ec_public_key key = {}; + memcpy(key.data, accountSignatureKey.c_str(), sizeof(key.data)); + if (1 != curve_verify_signature(&key, (BYTE *)buf.data(), buf.length(), (BYTE *)accountSignature.c_str(), accountSignature.size())) + throw "OnIqPairSuccess: got reply with invalid account signature, exiting"; + } + debugLogA("Received valid account signature"); + { + MBinBuffer buf; + buf.append("\x06\x01", 2); + buf.append(deviceDetails.c_str(), deviceDetails.size()); + buf.append(m_noise->signedIdentity.pub.data(), m_noise->signedIdentity.pub.length()); + buf.append(accountSignatureKey.c_str(), accountSignatureKey.size()); + + signal_buffer *result; + ec_private_key key = {}; + memcpy(key.data, m_noise->signedIdentity.priv.data(), m_noise->signedIdentity.priv.length()); + if (curve_calculate_signature(g_plugin.pCtx, &result, &key, (BYTE *)buf.data(), buf.length()) != 0) + throw "OnIqPairSuccess: cannot calculate account signature, exiting"; + + account.set_devicesignature(result->data, result->len); + signal_buffer_free(result); + } + + setDword("SignalDeviceId", 0); + { + MBinBuffer key; + if (accountSignatureKey.size() == 32) + key.append(KEY_BUNDLE_TYPE, 1); + key.append(accountSignatureKey.c_str(), accountSignatureKey.size()); + db_set_blob(0, m_szModuleName, "SignalIdentifierKey", key.data(), (int)key.length()); + } + + account.clear_accountsignaturekey(); + + MBinBuffer accountEnc(account.ByteSize()); + account.SerializeToArray(accountEnc.data(), (int)accountEnc.length()); + db_set_blob(0, m_szModuleName, "WAAccount", accountEnc.data(), (int)accountEnc.length()); + + proto::ADVDeviceIdentity deviceIdentity; + deviceIdentity.ParseFromString(deviceDetails); + + WANode reply("iq"); + reply << CHAR_PARAM("to", S_WHATSAPP_NET) << CHAR_PARAM("type", "result") << CHAR_PARAM("id", node.getAttr("id")); + + WANode *nodePair = reply.addChild("pair-device-sign"); + + WANode *nodeDeviceIdentity = nodePair->addChild("device-identity"); + nodeDeviceIdentity->addAttr("key-index", deviceIdentity.keyindex()); + nodeDeviceIdentity->content.append(accountEnc.data(), accountEnc.length()); + WSSendNode(reply); + } + else throw "OnIqPairSuccess: got reply without identity, exiting"; + } + catch (const char *pErrMsg) { + debugLogA(pErrMsg); + ShutdownSession(); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnProcessHandshake(const void *pData, int cbLen) +{ + proto::HandshakeMessage msg; + if (!msg.ParseFromArray(pData, cbLen)) { + debugLogA("Error parsing data, exiting"); + +LBL_Error: + ShutdownSession(); + return; + } + + auto &static_ = msg.serverhello().static_(); + auto &payload_ = msg.serverhello().payload(); + auto &ephemeral_ = msg.serverhello().ephemeral(); + + m_noise->updateHash(ephemeral_.c_str(), ephemeral_.size()); + m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), ephemeral_.c_str()); + + MBinBuffer decryptedStatic = m_noise->decrypt(static_.c_str(), static_.size()); + m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), decryptedStatic.data()); + + MBinBuffer decryptedCert = m_noise->decrypt(payload_.c_str(), payload_.size()); + + proto::CertChain cert; cert.ParseFromArray(decryptedCert.data(), (int)decryptedCert.length()); + proto::CertChain::NoiseCertificate::Details details; details.ParseFromString(cert.intermediate().details()); + if (details.issuerserial() != 0) { + debugLogA("Invalid certificate serial number, exiting"); + goto LBL_Error; + } + + MBinBuffer encryptedPub = m_noise->encrypt(m_noise->noiseKeys.pub.data(), m_noise->noiseKeys.pub.length()); + m_noise->mixIntoKey(m_noise->noiseKeys.priv.data(), ephemeral_.c_str()); + + // create reply + proto::ClientPayload node; + + MFileVersion v; + Miranda_GetFileVersion(&v); + + // not received our jid from server? generate registration packet then + if (m_szJid.IsEmpty()) { + uint8_t appVersion[16]; + mir_md5_hash((BYTE *)APP_VERSION, sizeof(APP_VERSION) - 1, appVersion); + + auto *pAppVersion = new proto::DeviceProps_AppVersion(); + pAppVersion->set_primary(v[0]); + pAppVersion->set_secondary(v[1]); + pAppVersion->set_tertiary(v[2]); + pAppVersion->set_quaternary(v[3]); + + proto::DeviceProps pCompanion; + pCompanion.set_os("Miranda"); + pCompanion.set_allocated_version(pAppVersion); + pCompanion.set_platformtype(proto::DeviceProps_PlatformType_DESKTOP); + pCompanion.set_requirefullsync(false); + + MBinBuffer buf(pCompanion.ByteSize()); + pCompanion.SerializeToArray(buf.data(), (int)buf.length()); + + auto *pPairingData = new proto::ClientPayload_DevicePairingRegistrationData(); + pPairingData->set_deviceprops(buf.data(), buf.length()); + pPairingData->set_buildhash(appVersion, sizeof(appVersion)); + pPairingData->set_eregid(encodeBigEndian(getDword(DBKEY_REG_ID))); + pPairingData->set_ekeytype(KEY_BUNDLE_TYPE); + pPairingData->set_eident(m_noise->signedIdentity.pub.data(), m_noise->signedIdentity.pub.length()); + pPairingData->set_eskeyid(encodeBigEndian(m_noise->preKey.keyid)); + pPairingData->set_eskeyval(m_noise->preKey.pub.data(), m_noise->preKey.pub.length()); + pPairingData->set_eskeysig(m_noise->preKey.signature.data(), m_noise->preKey.signature.length()); + node.set_allocated_devicepairingdata(pPairingData); + + node.set_passive(false); + } + // generate login packet + else { + WAJid jid(m_szJid); + node.set_username(_atoi64(jid.user)); + node.set_device(getDword(DBKEY_DEVICE_ID)); + node.set_passive(true); + } + + auto *pUserVersion = new proto::ClientPayload_UserAgent_AppVersion(); + pUserVersion->set_primary(2); + pUserVersion->set_secondary(2230); + pUserVersion->set_tertiary(15); + + auto *pUserAgent = new proto::ClientPayload_UserAgent(); + pUserAgent->set_allocated_appversion(pUserVersion); + pUserAgent->set_platform(proto::ClientPayload_UserAgent_Platform_WEB); + pUserAgent->set_releasechannel(proto::ClientPayload_UserAgent_ReleaseChannel_RELEASE); + pUserAgent->set_mcc("000"); + pUserAgent->set_mnc("000"); + pUserAgent->set_osversion("0.1"); + pUserAgent->set_osbuildnumber("0.1"); + pUserAgent->set_manufacturer(""); + pUserAgent->set_device("Desktop"); + pUserAgent->set_localelanguageiso6391("en"); + pUserAgent->set_localecountryiso31661alpha2("US"); + + auto *pWebInfo = new proto::ClientPayload_WebInfo(); + pWebInfo->set_websubplatform(proto::ClientPayload_WebInfo_WebSubPlatform_WEB_BROWSER); + + node.set_connecttype(proto::ClientPayload_ConnectType_WIFI_UNKNOWN); + node.set_connectreason(proto::ClientPayload_ConnectReason_USER_ACTIVATED); + node.set_allocated_useragent(pUserAgent); + node.set_allocated_webinfo(pWebInfo); + + MBinBuffer payload(node.ByteSize()); + node.SerializeToArray(payload.data(), (int)payload.length()); + + MBinBuffer payloadEnc = m_noise->encrypt(payload.data(), payload.length()); + + auto *pFinish = new proto::HandshakeMessage_ClientFinish(); + pFinish->set_payload(payloadEnc.data(), payloadEnc.length()); + pFinish->set_static_(encryptedPub.data(), encryptedPub.length()); + + proto::HandshakeMessage handshake; + handshake.set_allocated_clientfinish(pFinish); + WSSend(handshake); + + m_noise->finish(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::InitPersistentHandlers() +{ + m_arPersistent.insert(new WAPersistentHandler("iq", "md", "pair-device", &WhatsAppProto::OnIqPairDevice)); + m_arPersistent.insert(new WAPersistentHandler("iq", "md", "pair-success", &WhatsAppProto::OnIqPairSuccess)); + + m_arPersistent.insert(new WAPersistentHandler("stream:error", nullptr, nullptr, &WhatsAppProto::OnStreamError)); +} diff --git a/protocols/WhatsAppWeb/src/main.cpp b/protocols/WhatsAppWeb/src/main.cpp index b7a8a790ed..445f22edb3 100644 --- a/protocols/WhatsAppWeb/src/main.cpp +++ b/protocols/WhatsAppWeb/src/main.cpp @@ -39,7 +39,7 @@ extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOC CMPlugin::CMPlugin() : ACCPROTOPLUGIN(MODULENAME, pluginInfo) { - SetUniqueId(DBKEY_ID); + SetUniqueId(DBKEY_JID); } ///////////////////////////////////////////////////////////////////////////////////////// diff --git a/protocols/WhatsAppWeb/src/noise.cpp b/protocols/WhatsAppWeb/src/noise.cpp index 72b64fcc8a..68f2549bf9 100644 --- a/protocols/WhatsAppWeb/src/noise.cpp +++ b/protocols/WhatsAppWeb/src/noise.cpp @@ -30,9 +30,6 @@ WANoise::WANoise(WhatsAppProto *_ppro) : ephemeral.priv.assign(pPrivKey->data, sizeof(pPrivKey->data)); ec_key_pair_destroy(pKeys); - // ephemeral.pub.assign("\xd7\x58\xeb\xcc\x79\xb8\x58\xde\xc7\x60\x5c\x12\x22\xc1\x3b\x7c\xf6\x73\x38\x0b\x89\x56\xf1\xe2\xa1\xb0\xaa\x3a\xba\xbc\x08\x3f", 32); - // ephemeral.priv.assign("\xa0\xef\xd2\xbd\x2d\x4a\x6f\x9c\xd0\x9e\xc5\x75\x3c\x78\x78\xed\xe5\xec\x99\xd7\x4b\xeb\xf8\xb0\xdd\x1e\xe2\xc1\x85\xc4\xd8\x72", 32); - // prepare hash memcpy(hash, noise_init, 32); updateHash(intro_header, 4); diff --git a/protocols/WhatsAppWeb/src/proto.cpp b/protocols/WhatsAppWeb/src/proto.cpp index c3d965e919..8e94628b92 100644 --- a/protocols/WhatsAppWeb/src/proto.cpp +++ b/protocols/WhatsAppWeb/src/proto.cpp @@ -30,6 +30,7 @@ static int CompareUsers(const WAUser *p1, const WAUser *p2) WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) : PROTO(proto_name, username), m_impl(*this), + m_szJid(getMStringA(DBKEY_JID)), m_tszDefaultGroup(getWStringA(DBKEY_DEF_GROUP)), m_arUsers(10, CompareUsers), m_arOwnMsgs(1, CompareOwnMsgs), @@ -49,18 +50,7 @@ WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) : HookProtoEvent(ME_OPT_INITIALISE, &WhatsAppProto::OnOptionsInit); - m_arPersistent.insert(new WAPersistentHandler("iq", "md", "pair-device", &WhatsAppProto::OnIqPairDevice)); - m_arPersistent.insert(new WAPersistentHandler("iq", "md", "pair-success", &WhatsAppProto::OnIqPairSuccess)); - - // Client id generation - m_szClientId = getMStringA(DBKEY_CLIENT_ID); - if (m_szClientId.IsEmpty()) { - int8_t randBytes[16]; - Utils_GetRandom(randBytes, sizeof(randBytes)); - - m_szClientId = ptrA(mir_base64_encode(randBytes, sizeof(randBytes))); - setString(DBKEY_CLIENT_ID, m_szClientId); - } + InitPersistentHandlers(); // Create standard network connection wchar_t descr[512]; @@ -72,6 +62,7 @@ WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) : nlu.szDescriptiveName.w = descr; m_hNetlibUser = Netlib_RegisterUser(&nlu); + // Avatars folder m_tszAvatarFolder = CMStringW(VARSW(L"%miranda_avatarcache%")) + L"\\" + m_tszUserName; DWORD dwAttributes = GetFileAttributes(m_tszAvatarFolder.c_str()); if (dwAttributes == 0xffffffff || (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) @@ -101,7 +92,7 @@ void WhatsAppProto::OnModulesLoaded() { // initialize contacts cache for (auto &cc : AccContacts()) { - CMStringA szId(getMStringA(cc, isChatRoom(cc) ? "ChatRoomID" : DBKEY_ID)); + CMStringA szId(getMStringA(cc, isChatRoom(cc) ? "ChatRoomID" : DBKEY_JID)); if (!szId.IsEmpty()) m_arUsers.insert(new WAUser(cc, szId)); } @@ -225,7 +216,7 @@ void WhatsAppProto::OnSendMessage(const JSONNode &node, void*) int WhatsAppProto::SendMsg(MCONTACT hContact, int, const char *pszMsg) { - ptrA jid(getStringA(hContact, DBKEY_ID)); + ptrA jid(getStringA(hContact, DBKEY_JID)); if (jid == nullptr || pszMsg == nullptr) return 0; @@ -267,7 +258,7 @@ int WhatsAppProto::SendMsg(MCONTACT hContact, int, const char *pszMsg) int WhatsAppProto::UserIsTyping(MCONTACT hContact, int) { if (hContact && isOnline()) { - ptrA jid(getStringA(hContact, DBKEY_ID)); + ptrA jid(getStringA(hContact, DBKEY_JID)); if (jid && isOnline()) { } } diff --git a/protocols/WhatsAppWeb/src/proto.h b/protocols/WhatsAppWeb/src/proto.h index 307615636b..b2e0bc6301 100644 --- a/protocols/WhatsAppWeb/src/proto.h +++ b/protocols/WhatsAppWeb/src/proto.h @@ -139,10 +139,10 @@ class WhatsAppProto : public PROTO } m_impl; - bool m_bTerminated; + bool m_bTerminated, m_bRespawn; ptrW m_tszDefaultGroup; - CMStringA m_szJid, m_szClientId, m_szClientToken; + CMStringA m_szJid; CMStringW m_tszAvatarFolder; EVP_PKEY *m_pKeys; // private & public keys @@ -187,7 +187,7 @@ class WhatsAppProto : public PROTO void OnLoggedIn(void); void OnLoggedOut(void); - bool ServerThreadWorker(void); + void ServerThreadWorker(void); void ShutdownSession(void); void SendKeepAlive(); @@ -200,20 +200,15 @@ class WhatsAppProto : public PROTO void OnProcessHandshake(const void *pData, int cbLen); + void InitPersistentHandlers(); void OnIqPairDevice(const WANode &node); void OnIqPairSuccess(const WANode &node); + void OnStreamError(const WANode &node); // binary packets void ProcessBinaryPacket(const void *pData, size_t cbLen); void ProcessBinaryNode(const WANode &node); - // text packets - void ProcessPacket(const JSONNode &node); - void ProcessBlocked(const JSONNode &node); - void ProcessCmd(const JSONNode &node); - void ProcessConn(const JSONNode &node); - void ProcessPresence(const JSONNode &node); - /// Avatars //////////////////////////////////////////////////////////////////////////// CMStringW GetAvatarFileName(MCONTACT hContact); diff --git a/protocols/WhatsAppWeb/src/server.cpp b/protocols/WhatsAppWeb/src/server.cpp index 63979d9cc3..126d73e956 100644 --- a/protocols/WhatsAppWeb/src/server.cpp +++ b/protocols/WhatsAppWeb/src/server.cpp @@ -7,355 +7,21 @@ Copyright #include "stdafx.h" -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnLoggedIn() -{ - debugLogA("WhatsAppProto::OnLoggedIn"); - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); - m_iStatus = m_iDesiredStatus; - - SendKeepAlive(); - m_impl.m_keepAlive.Start(60000); -} - -void WhatsAppProto::OnLoggedOut(void) -{ - debugLogA("WhatsAppProto::OnLoggedOut"); - m_bTerminated = true; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - - setAllContactStatuses(ID_STATUS_OFFLINE, false); -} - -void WhatsAppProto::SendKeepAlive() -{ - WebSocket_SendText(m_hServerConn, "?,,"); - - time_t now = time(0); - - for (auto &it : m_arUsers) { - if (it->m_time1 && now - it->m_time1 >= 1200) { // 20 minutes - setWord(it->hContact, "Status", ID_STATUS_NA); - it->m_time1 = 0; - it->m_time2 = now; - } - else if (it->m_time2 && now - it->m_time2 >= 1200) { // 20 minutes - setWord(it->hContact, "Status", ID_STATUS_OFFLINE); - it->m_time2 = 0; - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::ShutdownSession() -{ - if (m_bTerminated) - return; - - debugLogA("WhatsAppProto::ShutdownSession"); - - // shutdown all resources - if (m_hServerConn) - Netlib_Shutdown(m_hServerConn); - - OnLoggedOut(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqPairDevice(const WANode &node) -{ - WANode reply("iq"); - reply << CHAR_PARAM("to", S_WHATSAPP_NET) << CHAR_PARAM("type", "result") << CHAR_PARAM("id", node.getAttr("id")); - WSSendNode(reply); - - if (auto *pRef = node.getChild("pair-device")->getChild("ref")) { - ShowQrCode(pRef->getBody()); - } - else { - debugLogA("OnIqPairDevice: got reply without ref, exiting"); - ShutdownSession(); - } -} - -void WhatsAppProto::OnIqPairSuccess(const WANode &node) -{ - CloseQrDialog(); - - auto *pRoot = node.getChild("pair-success"); - - try { - if (auto *pPlatform = pRoot->getChild("platform")) - debugLogA("Got response from platform: %s", pPlatform->getBody().c_str()); - - if (auto *pBiz = pRoot->getChild("biz")) - if (auto *pszName = pBiz->getAttr("name")) - setUString("Nick", pszName); - - if (auto *pDevice = pRoot->getChild("device")) { - if (auto *pszJid = pDevice->getAttr("jid")) - setUString("jid", pszJid); - } - else throw "OnIqPairSuccess: got reply without device info, exiting"; - - if (auto *pIdentity = pRoot->getChild("device-identity")) { - proto::ADVSignedDeviceIdentityHMAC payload; - if (!payload.ParseFromArray(pIdentity->content.data(), (int)pIdentity->content.length())) - throw "OnIqPairSuccess: got reply with invalid identity, exiting"; - - auto &hmac = payload.hmac(); - debugLogA("Received HMAC signature: %s", hmac.c_str()); - - auto &details = payload.details(); - Netlib_Dump(nullptr, details.c_str(), details.size(), false, 0); - - // check details signature using HMAC - { - uint8_t signature[32]; - unsigned int out_len = sizeof(signature); - MBinBuffer secret(getBlob(DBKEY_SECRET_KEY)); - HMAC(EVP_sha256(), secret.data(), (int)secret.length(), (BYTE *)details.c_str(), (int)details.size(), signature, &out_len); - if (memcmp(hmac.c_str(), signature, sizeof(signature))) - throw "OnIqPairSuccess: got reply with invalid details signature, exiting"; - } - - proto::ADVSignedDeviceIdentity account; - if (!account.ParseFromString(details)) - throw "OnIqPairSuccess: got reply with invalid account, exiting"; - - auto &deviceDetails = account.details(); - auto &accountSignature = account.accountsignature(); - auto &accountSignatureKey = account.accountsignaturekey(); - { - MBinBuffer buf; - buf.append("\x06\x00", 2); - buf.append(deviceDetails.c_str(), deviceDetails.size()); - buf.append(m_noise->signedIdentity.pub.data(), m_noise->signedIdentity.pub.length()); - - ec_public_key key = {}; - memcpy(key.data, accountSignatureKey.c_str(), sizeof(key.data)); - if (1 != curve_verify_signature(&key, (BYTE *)buf.data(), buf.length(), (BYTE *)accountSignature.c_str(), accountSignature.size())) - throw "OnIqPairSuccess: got reply with invalid account signature, exiting"; - } - debugLogA("Received valid account signature"); - { - MBinBuffer buf; - buf.append("\x06\x01", 2); - buf.append(deviceDetails.c_str(), deviceDetails.size()); - buf.append(m_noise->signedIdentity.pub.data(), m_noise->signedIdentity.pub.length()); - buf.append(accountSignatureKey.c_str(), accountSignatureKey.size()); - - signal_buffer *result; - ec_private_key key = {}; - memcpy(key.data, m_noise->signedIdentity.priv.data(), m_noise->signedIdentity.priv.length()); - if (curve_calculate_signature(g_plugin.pCtx, &result, &key, (BYTE *)buf.data(), buf.length()) != 0) - throw "OnIqPairSuccess: cannot calculate account signature, exiting"; - - account.set_devicesignature(result->data, result->len); - signal_buffer_free(result); - } - - setDword("SignalDeviceId", 0); - { - MBinBuffer key; - if (accountSignatureKey.size() == 32) - key.append(KEY_BUNDLE_TYPE, 1); - key.append(accountSignatureKey.c_str(), accountSignatureKey.size()); - db_set_blob(0, m_szModuleName, "SignalIdentifierKey", key.data(), (int)key.length()); - } - - account.clear_accountsignaturekey(); - - MBinBuffer accountEnc(account.ByteSize()); - account.SerializeToArray(accountEnc.data(), (int)accountEnc.length()); - db_set_blob(0, m_szModuleName, "WAAccount", accountEnc.data(), (int)accountEnc.length()); - - proto::ADVDeviceIdentity deviceIdentity; - deviceIdentity.ParseFromString(deviceDetails); - - WANode reply("iq"); - reply << CHAR_PARAM("to", S_WHATSAPP_NET) << CHAR_PARAM("type", "result") << CHAR_PARAM("id", node.getAttr("id")); - - WANode *nodePair = reply.addChild("pair-device-sign"); - - WANode *nodeDeviceIdentity = nodePair->addChild("device-identity"); - nodeDeviceIdentity->addAttr("key-index", deviceIdentity.keyindex()); - nodeDeviceIdentity->content.append(accountEnc.data(), accountEnc.length()); - WSSendNode(reply); - } - else throw "OnIqPairSuccess: got reply without identity, exiting"; - } - catch (const char *pErrMsg) { - debugLogA(pErrMsg); - ShutdownSession(); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnProcessHandshake(const void *pData, int cbLen) -{ - proto::HandshakeMessage msg; - if (!msg.ParseFromArray(pData, cbLen)) { - debugLogA("Error parsing data, exiting"); - -LBL_Error: - ShutdownSession(); - return; - } - - auto &static_ = msg.serverhello().static_(); - auto &payload_ = msg.serverhello().payload(); - auto &ephemeral_ = msg.serverhello().ephemeral(); - - // std::string ephemeral_("\xe5\x3d\xff\xc6\x59\xef\xa7\x64\xf6\x48\xf3\x46\x15\xb4\x4f\x13\xfa\xc6\x29\x18\x34\xd4\xa4\xa9\xad\xa0\xa3\x05\xaa\x4d\xda\x36", 32); - // std::string static_("\xa9\x70\xc0\xfe\xe0\x72\x3b\x0a\x6e\x0a\xd4\xe1\xb4\x5f\xcd\xb8\x06\x68\xdd\x6b\x45\x36\xe0\x7d\x0d\xe9\x00\x4d\xb6\xaf\xfa\xa3\xb7\x54\x82\x24\xa9\xe4\x2c\xd4\xe5\xd0\x2f\xd6\x31\x2b\xca\xec", 48); - // std::string payload_("\x11\xcc\xb8\x74\xe6\x27\x29\x37\x65\xb2\x9e\x47\x53\x89\xf7\xce\x23\x03\x9c\xd4\x9e\x4b\x12\xdc\x3f\x10\xe4\x68\xfe\xfd\x31\x80\x1d\x48\x01\x9c\x31\xef\x54\xdb\xa1\x8f\xdb\x6b\x53\x84\xbb\x6d\xb4\x0c\x61\x1f\xcd\xe7\x3c\x0e\xe2\x18\xe4\x95\xf7\xbc\x5b\xbf\x80\x93\x21\x98\x80\x20\x9b\x71\x27\x47\x39\xe9\x04\x08\x5d\xd2\x62\x48\xf6\x23\xba\xa0\x31\xfc\x7c\xeb\xa0\xaa\x56\x04\x71\x71\x84\x9b\x08\xa4\xc9\x33\xd5\x07\x04\x5f\x1c\xd2\x6f\x7d\x5d\x83\x29\x5f\x80\x4a\xbf\x7c\xd9\x7c\xd0\x2b\x9a\x1e\xe0\x28\x33\x89\xb5\x3b\xd2\xe7\x7f\xfc\xd6\xa8\x55\xe2\x9c\x6e\x5f\x6f\x08\xa1\xf3\xfd\x5e\xff\x56\xee\x6f\x31\x47\xeb\xd4\x07\x92\x81\x72\x68\x91\x9d\xe9\xb3\x5f\x1d\x61\x8e\xce\x55\x4b\xbe\x74\x5d\xef\xea\xe4\x23\x63\x0d\x7c\xd4\xa3\xf8\xa1\x29\x60\xd7\x2c\xe8\xfc\xb5\x89\x99\x32\x95\xfc\xec\x6f\x7b\x2a\x23\xf4\x75\xbe\xe3\x21\x6a\x71\x3f\xc4\x1b\x99\x9f\x42\xd5\x19\xd8\xcc\xe7\xab\x90\xdd\xe5\xd8\xa5\xe3\xb5\x5c\xa2\x54\xf3\x4b\x0c\xa1\xe2\xa2\x91\xa3\xd0\x92\x6d\xfa\xab\x5a\xf6\x80\xee\x84\xbe\xaa\x75\x5e\xee\x6b\x91\x49", 257); - - m_noise->updateHash(ephemeral_.c_str(), ephemeral_.size()); - m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), ephemeral_.c_str()); - - MBinBuffer decryptedStatic = m_noise->decrypt(static_.c_str(), static_.size()); - m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), decryptedStatic.data()); - - MBinBuffer decryptedCert = m_noise->decrypt(payload_.c_str(), payload_.size()); - - proto::CertChain cert; cert.ParseFromArray(decryptedCert.data(), (int)decryptedCert.length()); - proto::CertChain::NoiseCertificate::Details details; details.ParseFromString(cert.intermediate().details()); - if (details.issuerserial() != 0) { - debugLogA("Invalid certificate serial number, exiting"); - goto LBL_Error; - } - - MBinBuffer encryptedPub = m_noise->encrypt(m_noise->noiseKeys.pub.data(), m_noise->noiseKeys.pub.length()); - m_noise->mixIntoKey(m_noise->noiseKeys.priv.data(), ephemeral_.c_str()); - - // create reply - proto::ClientPayload node; - - MFileVersion v; - Miranda_GetFileVersion(&v); - - // generate registration packet - if (m_szClientToken.IsEmpty()) { - uint8_t appVersion[16]; - mir_md5_hash((BYTE*)APP_VERSION, sizeof(APP_VERSION) - 1, appVersion); - - auto *pAppVersion = new proto::DeviceProps_AppVersion(); - pAppVersion->set_primary(v[0]); - pAppVersion->set_secondary(v[1]); - pAppVersion->set_tertiary(v[2]); - pAppVersion->set_quaternary(v[3]); - - proto::DeviceProps pCompanion; - pCompanion.set_os("Miranda"); - pCompanion.set_allocated_version(pAppVersion); - pCompanion.set_platformtype(proto::DeviceProps_PlatformType_DESKTOP); - pCompanion.set_requirefullsync(false); - - MBinBuffer buf(pCompanion.ByteSize()); - pCompanion.SerializeToArray(buf.data(), (int)buf.length()); - - auto *pPairingData = new proto::ClientPayload_DevicePairingRegistrationData(); - pPairingData->set_deviceprops(buf.data(), buf.length()); - pPairingData->set_buildhash(appVersion, sizeof(appVersion)); - pPairingData->set_eregid(encodeBigEndian(getDword(DBKEY_REG_ID))); - pPairingData->set_ekeytype(KEY_BUNDLE_TYPE); - pPairingData->set_eident(m_noise->signedIdentity.pub.data(), m_noise->signedIdentity.pub.length()); - pPairingData->set_eskeyid(encodeBigEndian(m_noise->preKey.keyid)); - pPairingData->set_eskeyval(m_noise->preKey.pub.data(), m_noise->preKey.pub.length()); - pPairingData->set_eskeysig(m_noise->preKey.signature.data(), m_noise->preKey.signature.length()); - node.set_allocated_devicepairingdata(pPairingData); - - node.set_passive(false); - } - // generate login packet - else { - node.set_passive(true); - } - - auto *pUserVersion = new proto::ClientPayload_UserAgent_AppVersion(); - pUserVersion->set_primary(2); - pUserVersion->set_secondary(2230); - pUserVersion->set_tertiary(15); - - auto *pUserAgent = new proto::ClientPayload_UserAgent(); - pUserAgent->set_allocated_appversion(pUserVersion); - pUserAgent->set_platform(proto::ClientPayload_UserAgent_Platform_WEB); - pUserAgent->set_releasechannel(proto::ClientPayload_UserAgent_ReleaseChannel_RELEASE); - pUserAgent->set_mcc("000"); - pUserAgent->set_mnc("000"); - pUserAgent->set_osversion("0.1"); - pUserAgent->set_osbuildnumber("0.1"); - pUserAgent->set_manufacturer(""); - pUserAgent->set_device("Desktop"); - pUserAgent->set_localelanguageiso6391("en"); - pUserAgent->set_localecountryiso31661alpha2("US"); - - auto *pWebInfo = new proto::ClientPayload_WebInfo(); - pWebInfo->set_websubplatform(proto::ClientPayload_WebInfo_WebSubPlatform_WEB_BROWSER); - - node.set_connecttype(proto::ClientPayload_ConnectType_WIFI_UNKNOWN); - node.set_connectreason(proto::ClientPayload_ConnectReason_USER_ACTIVATED); - node.set_allocated_useragent(pUserAgent); - node.set_allocated_webinfo(pWebInfo); - - MBinBuffer payload(node.ByteSize()); - node.SerializeToArray(payload.data(), (int)payload.length()); - - MBinBuffer payloadEnc = m_noise->encrypt(payload.data(), payload.length()); - - auto *pFinish = new proto::HandshakeMessage_ClientFinish(); - pFinish->set_payload(payloadEnc.data(), payloadEnc.length()); - pFinish->set_static_(encryptedPub.data(), encryptedPub.length()); - - proto::HandshakeMessage handshake; - handshake.set_allocated_clientfinish(pFinish); - WSSend(handshake); - - m_noise->finish(); -} - ///////////////////////////////////////////////////////////////////////////////////////// // gateway worker thread -bool WhatsAppProto::WSReadPacket(const WSHeader &hdr, MBinBuffer &res) -{ - size_t currPacketSize = res.length() - hdr.headerSize; - - char buf[1024]; - while (currPacketSize < hdr.payloadSize) { - int result = Netlib_Recv(m_hServerConn, buf, _countof(buf), MSG_NODUMP); - if (result == 0) { - debugLogA("Gateway connection gracefully closed"); - return false; - } - if (result < 0) { - debugLogA("Gateway connection error, exiting"); - return false; - } - - currPacketSize += result; - res.append(buf, result); - } - return true; -} - void WhatsAppProto::ServerThread(void *) { - m_bTerminated = false; + do { + m_bRespawn = false; + ServerThreadWorker(); + } + while (m_bRespawn); - while (ServerThreadWorker()) - ; ShutdownSession(); } -bool WhatsAppProto::ServerThreadWorker() +void WhatsAppProto::ServerThreadWorker() { // connect websocket NETLIBHTTPHEADER hdrs[] = @@ -367,11 +33,11 @@ bool WhatsAppProto::ServerThreadWorker() NLHR_PTR pReply(WebSocket_Connect(m_hNetlibUser, "web.whatsapp.com/ws/chat", hdrs)); if (pReply == nullptr) { debugLogA("Server connection failed, exiting"); - return false; + return; } if (pReply->resultCode != 101) - return false; + return; delete m_noise; m_noise = new WANoise(this); @@ -380,17 +46,15 @@ bool WhatsAppProto::ServerThreadWorker() debugLogA("Server connection succeeded"); m_hServerConn = pReply->nlc; m_iLoginTime = time(0); - m_szClientToken = getMStringA(DBKEY_CLIENT_TOKEN); auto &pubKey = m_noise->ephemeral.pub; auto *client = new proto::HandshakeMessage::ClientHello(); client->set_ephemeral(pubKey.data(), pubKey.length()); proto::HandshakeMessage msg; msg.set_allocated_clienthello(client); WSSend(msg); - bool bExit = false; MBinBuffer netbuf; - while (!bExit && !m_bTerminated) { + for (m_bTerminated = false; !m_bTerminated;) { unsigned char buf[2048]; int bufSize = Netlib_Recv(m_hServerConn, (char *)buf, _countof(buf), MSG_NODUMP); if (bufSize == 0) { @@ -452,7 +116,6 @@ bool WhatsAppProto::ServerThreadWorker() if (pReq != nullptr) { root << CHAR_PARAM("$id$", szPrefix); } - else ProcessPacket(root); } } } @@ -460,7 +123,7 @@ bool WhatsAppProto::ServerThreadWorker() case 8: // close debugLogA("server required to exit"); - bExit = true; // simply reconnect, don't exit + m_bRespawn = m_bTerminated = true; // simply reconnect, don't exit break; default: @@ -492,7 +155,28 @@ bool WhatsAppProto::ServerThreadWorker() debugLogA("Server connection dropped"); Netlib_CloseHandle(m_hServerConn); m_hServerConn = nullptr; - return false; +} + +bool WhatsAppProto::WSReadPacket(const WSHeader &hdr, MBinBuffer &res) +{ + size_t currPacketSize = res.length() - hdr.headerSize; + + char buf[1024]; + while (currPacketSize < hdr.payloadSize) { + int result = Netlib_Recv(m_hServerConn, buf, _countof(buf), MSG_NODUMP); + if (result == 0) { + debugLogA("Gateway connection gracefully closed"); + return false; + } + if (result < 0) { + debugLogA("Gateway connection error, exiting"); + return false; + } + + currPacketSize += result; + res.append(buf, result); + } + return true; } ///////////////////////////////////////////////////////////////////////////////////////// @@ -549,67 +233,60 @@ void WhatsAppProto::ProcessBinaryNode(const WANode &node) } ///////////////////////////////////////////////////////////////////////////////////////// -// Json data processing -void WhatsAppProto::ProcessPacket(const JSONNode &root) +void WhatsAppProto::OnLoggedIn() { - CMStringA szType = root[(size_t)0].as_mstring(); - const JSONNode &content = root[1]; - - if (szType == "Conn") - ProcessConn(content); - else if (szType == "Cmd") - ProcessCmd(content); - else if (szType == "Presence") - ProcessPresence(content); - else if (szType == "Blocklist") - ProcessBlocked(content); -} + debugLogA("WhatsAppProto::OnLoggedIn"); -void WhatsAppProto::ProcessBlocked(const JSONNode &node) -{ - for (auto &it : node["blocklist"]) { - auto *pUser = AddUser(it.as_string().c_str(), false); - Ignore_Ignore(pUser->hContact, IGNOREEVENT_ALL); - Contact::Hide(pUser->hContact); - } -} + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); + m_iStatus = m_iDesiredStatus; -void WhatsAppProto::ProcessCmd(const JSONNode &root) -{ - CMStringW wszType(root["type"].as_mstring()); - if (wszType == L"challenge") { - } + SendKeepAlive(); + m_impl.m_keepAlive.Start(60000); } -void WhatsAppProto::ProcessConn(const JSONNode &root) +void WhatsAppProto::OnLoggedOut(void) { - CloseQrDialog(); - - writeStr(DBKEY_ID, root["wid"]); - m_szJid = getMStringA(DBKEY_ID); + debugLogA("WhatsAppProto::OnLoggedOut"); + m_bTerminated = true; - writeStr(DBKEY_NICK, root["pushname"]); - writeStr(DBKEY_CLIENT_TOKEN, root["clientToken"]); - writeStr(DBKEY_SERVER_TOKEN, root["serverToken"]); - writeStr(DBKEY_BROWSER_TOKEN, root["browserToken"]); + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - OnLoggedIn(); + setAllContactStatuses(ID_STATUS_OFFLINE, false); } -void WhatsAppProto::ProcessPresence(const JSONNode &root) +void WhatsAppProto::SendKeepAlive() { - CMStringA jid = root["id"].as_mstring(); - if (auto *pUser = FindUser(jid)) { - CMStringA state(root["type"].as_mstring()); - DWORD timestamp(_wtoi(root["t"].as_mstring())); - if (state == "available") { - setWord(pUser->hContact, "Status", ID_STATUS_ONLINE); + WebSocket_SendText(m_hServerConn, "?,,"); + + time_t now = time(0); + + for (auto &it : m_arUsers) { + if (it->m_time1 && now - it->m_time1 >= 1200) { // 20 minutes + setWord(it->hContact, "Status", ID_STATUS_NA); + it->m_time1 = 0; + it->m_time2 = now; } - else if (state == "unavailable") { - setWord(pUser->hContact, "Status", ID_STATUS_AWAY); - pUser->m_time1 = timestamp; + else if (it->m_time2 && now - it->m_time2 >= 1200) { // 20 minutes + setWord(it->hContact, "Status", ID_STATUS_OFFLINE); + it->m_time2 = 0; } } - else debugLogA("Presence from unknown contact %s ignored", jid.c_str()); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::ShutdownSession() +{ + if (m_bTerminated) + return; + + debugLogA("WhatsAppProto::ShutdownSession"); + + // shutdown all resources + if (m_hServerConn) + Netlib_Shutdown(m_hServerConn); + + OnLoggedOut(); } diff --git a/protocols/WhatsAppWeb/src/stdafx.cxx b/protocols/WhatsAppWeb/src/stdafx.cxx index 46e6a71904..8784e33705 100644 --- a/protocols/WhatsAppWeb/src/stdafx.cxx +++ b/protocols/WhatsAppWeb/src/stdafx.cxx @@ -1,7 +1,7 @@ /* WhatsAppWeb plugin for Miranda NG -Copyright © 2019 George Hazan +Copyright © 2019-22 George Hazan */ diff --git a/protocols/WhatsAppWeb/src/utils.cpp b/protocols/WhatsAppWeb/src/utils.cpp index 79e5c0217a..7e45d65ea2 100644 --- a/protocols/WhatsAppWeb/src/utils.cpp +++ b/protocols/WhatsAppWeb/src/utils.cpp @@ -1,12 +1,59 @@ /* WhatsAppWeb plugin for Miranda NG -Copyright © 2019 George Hazan +Copyright © 2019-22 George Hazan */ #include "stdafx.h" +WAJid::WAJid(const char *pszUser, const char *pszServer, int iDevice, int iAgent) : + user(pszUser ? pszUser : ""), + server(pszServer ? pszServer : ""), + device(iDevice), + agent(iAgent) +{} + +WAJid::WAJid(const char *pszJid) +{ + if (pszJid == nullptr) + pszJid = ""; + + auto *tmp = NEWSTR_ALLOCA(pszJid); + auto *p = strrchr(tmp, '@'); + if (p) { + *p = 0; + server = p + 1; + } + + if (p = strrchr(tmp, ':')) { + *p = 0; + device = atoi(p + 1); + } + else device = 0; + + if (p = strrchr(tmp, '_')) { + *p = 0; + agent = atoi(p + 1); + } + else agent = 0; + + user = tmp; +} + +CMStringA WAJid::toString() const +{ + CMStringA ret(user); + if (agent > 0) + ret.AppendFormat("_%d", agent); + if (device > 0) + ret.AppendFormat(":%d", device); + ret.AppendFormat("@%s", server.c_str()); + return ret; +} + +///////////////////////////////////////////////////////////////////////////////////////// + WAUser* WhatsAppProto::FindUser(const char *szId) { mir_cslock lck(m_csUsers); @@ -23,7 +70,7 @@ WAUser* WhatsAppProto::AddUser(const char *szId, bool bTemporary) MCONTACT hContact = db_add_contact(); Proto_AddToContact(hContact, m_szModuleName); - setString(hContact, DBKEY_ID, szId); + setString(hContact, DBKEY_JID, szId); pUser = new WAUser(hContact, mir_strdup(szId)); if (bTemporary) Contact::RemoveFromList(hContact); diff --git a/protocols/WhatsAppWeb/src/utils.h b/protocols/WhatsAppWeb/src/utils.h index 507c674c1f..e517301b86 100644 --- a/protocols/WhatsAppWeb/src/utils.h +++ b/protocols/WhatsAppWeb/src/utils.h @@ -109,6 +109,19 @@ public: ///////////////////////////////////////////////////////////////////////////////////////// +struct WAJid +{ + int device, agent; + CMStringA user, server; + + WAJid(const char *pszJid); + WAJid(const char *pszUser, const char *pszServer, int device = 0, int agent = 0); + + CMStringA toString() const; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + std::string encodeBigEndian(uint32_t num, size_t len = sizeof(uint32_t)); void generateIV(uint8_t *iv, int &pVar); diff --git a/protocols/WhatsAppWeb/src/wanode.cpp b/protocols/WhatsAppWeb/src/wanode.cpp new file mode 100644 index 0000000000..72d8bec38c --- /dev/null +++ b/protocols/WhatsAppWeb/src/wanode.cpp @@ -0,0 +1,570 @@ +/* + +WhatsAppWeb plugin for Miranda NG +Copyright © 2019-22 George Hazan + +*/ + +#include "stdafx.h" +#include "dicts.h" + +struct Attr +{ + Attr(const char *pszName, const char *pszValue) : + name(pszName), + value(pszValue) + {} + + Attr(const char *pszName, int iValue) : + name(pszName), + value(FORMAT, "%d", iValue) + {} + + CMStringA name, value; +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// WANode members + +WANode::WANode() : + attrs(1), + children(1) +{} + +WANode::WANode(const char *pszTitle) : + attrs(1), + children(1), + title(pszTitle) +{} + +WANode::~WANode() +{ +} + +const char *WANode::getAttr(const char *pszName) const +{ + for (auto &p : attrs) + if (p->name == pszName) + return p->value.c_str(); + + return nullptr; +} + +void WANode::addAttr(const char *pszName, const char *pszValue) +{ + attrs.insert(new Attr(pszName, pszValue)); +} + +void WANode::addAttr(const char *pszName, int iValue) +{ + attrs.insert(new Attr(pszName, iValue)); +} + +CMStringA WANode::getBody() const +{ + return CMStringA((char *)content.data(), (int)content.length()); +} + +WANode *WANode::addChild(const char *pszName) +{ + auto *pNew = new WANode(pszName); + pNew->pParent = this; + children.insert(pNew); + return pNew; +} + +WANode* WANode::getChild(const char *pszName) const +{ + for (auto &it : children) + if (it->title == pszName) + return it; + + return nullptr; +} + +WANode* WANode::getFirstChild(void) const +{ + return (children.getCount()) ? &children[0] : nullptr; +} + +void WANode::print(CMStringA &dest, int level) const +{ + for (int i = 0; i < level; i++) + dest.Append(" "); + + dest.AppendFormat("<%s ", title.c_str()); + for (auto &p : attrs) + dest.AppendFormat("%s=\"%s\" ", p->name.c_str(), p->value.c_str()); + dest.Truncate(dest.GetLength() - 1); + + if (content.isEmpty() && !children.getCount()) { + dest.Append("/>\n"); + return; + } + + dest.Append(">"); + if (!content.isEmpty()) { + ptrA tmp((char *)mir_alloc(content.length() * 2 + 1)); + bin2hex(content.data(), content.length(), tmp); + dest.AppendFormat("%s", tmp.get()); + } + + if (children.getCount()) { + dest.Append("\n"); + + for (auto &p : children) + p->print(dest, level + 1); + + for (int i = 0; i < level; i++) + dest.Append(" "); + } + + dest.AppendFormat("\n", title.c_str()); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// WAReader class members + +bool WAReader::readAttributes(WANode *pNode, int count) +{ + if (count == 0) + return true; + + for (int i = 0; i < count; i++) { + CMStringA name = readString(readInt8()); + if (name.IsEmpty()) + return false; + + CMStringA value = readString(readInt8()); + if (value.IsEmpty()) + return false; + + pNode->addAttr(name, value); + } + return true; +} + +uint32_t WAReader::readInt20() +{ + if (m_limit - m_buf < 3) + return 0; + + int ret = (int(m_buf[0] & 0x0F) << 16) + (int(m_buf[1]) << 8) + int(m_buf[2]); + m_buf += 3; + return ret; +} + +uint32_t WAReader::readIntN(int n) +{ + if (m_limit - m_buf < n) + return 0; + + uint32_t res = 0; + for (int i = 0; i < n; i++, m_buf++) + res = (res <<= 8) + *m_buf; + return res; +} + +bool WAReader::readList(WANode *pParent, int tag) +{ + int size = readListSize(tag); + if (size == -1) + return false; + + for (int i = 0; i < size; i++) { + WANode *pNew = readNode(); + if (pNew == nullptr) + return false; + pParent->children.insert(pNew); + } + + return true; +} + +int WAReader::readListSize(int tag) +{ + switch (tag) { + case LIST_EMPTY: + return 0; + case LIST_8: + return readInt8(); + case LIST_16: + return readInt16(); + } + return -1; +} + +WANode *WAReader::readNode() +{ + int listSize = readListSize(readInt8()); + if (listSize == -1) + return nullptr; + + int descrTag = readInt8(); + if (descrTag == STREAM_END) + return nullptr; + + CMStringA name = readString(descrTag); + if (name.IsEmpty()) + return nullptr; + + std::unique_ptr ret(new WANode()); + ret->title = name.c_str(); + + if (!readAttributes(ret.get(), (listSize - 1) >> 1)) + return nullptr; + + if ((listSize % 2) == 1) + return ret.release(); + + int size, tag = readInt8(); + switch (tag) { + case LIST_EMPTY: case LIST_8: case LIST_16: + readList(ret.get(), tag); + break; + + case BINARY_8: + size = readInt8(); + +LBL_Binary: + if (m_limit - m_buf < size) + return false; + + ret->content.assign((void *)m_buf, size); + m_buf += size; + break; + + case BINARY_20: + size = readInt20(); + goto LBL_Binary; + + case BINARY_32: + size = readInt32(); + goto LBL_Binary; + + default: + CMStringA str = readString(tag); + ret->content.assign(str.GetBuffer(), str.GetLength() + 1); + } + + return ret.release(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static int unpackHex(int val) +{ + if (val < 0 || val > 15) + return -1; + + return (val < 10) ? val + '0' : val - 10 + 'A'; +} + +static int unpackNibble(int val) +{ + if (val < 0 || val > 15) + return -1; + + switch (val) { + case 10: return '-'; + case 11: return '.'; + case 15: return 0; + default: return '0' + val; + } +} + +CMStringA WAReader::readPacked(int tag) +{ + int startByte = readInt8(); + bool bTrim = false; + if (startByte & 0x80) { + startByte &= ~0x80; + bTrim = true; + } + + CMStringA ret; + for (int i = 0; i < startByte; i++) { + BYTE b = readInt8(); + int lower = (tag == NIBBLE_8) ? unpackNibble(b >> 4) : unpackHex(b >> 4); + if (lower == -1) + return ""; + + int higher = (tag == NIBBLE_8) ? unpackNibble(b & 0x0F) : unpackHex(b & 0x0F); + if (higher == -1) + return ""; + + ret.AppendChar(lower); + ret.AppendChar(higher); + } + + if (bTrim && !ret.IsEmpty()) + ret.Truncate(ret.GetLength() - 1); + return ret; +} + +CMStringA WAReader::readString(int tag) +{ + if (tag >= 1 && tag < _countof(SingleByteTokens)) + return SingleByteTokens[tag]; + + int idx; + switch (tag) { + case DICTIONARY_0: + idx = readInt8(); + return (idx < _countof(dict0)) ? dict0[idx] : ""; + + case DICTIONARY_1: + idx = readInt8(); + return (idx < _countof(dict1)) ? dict1[idx] : ""; + + case DICTIONARY_2: + idx = readInt8(); + return (idx < _countof(dict2)) ? dict2[idx] : ""; + + case DICTIONARY_3: + idx = readInt8(); + return (idx < _countof(dict3)) ? dict3[idx] : ""; + + case LIST_EMPTY: + return ""; + + case BINARY_8: + return readStringFromChars(readInt8()); + + case BINARY_20: + return readStringFromChars(readInt20()); + + case BINARY_32: + return readStringFromChars(readInt32()); + + case NIBBLE_8: + case HEX_8: + return readPacked(tag); + + case AD_JID: + { + int agent = readInt8(); + int device = readInt8(); + WAJid jid(readString(readInt8()), "s.whatsapp.net", device, agent); + return jid.toString(); + } + + case JID_PAIR: + CMStringA s1 = readString(readInt8()); + CMStringA s2 = readString(readInt8()); + if (s1.IsEmpty() && s2.IsEmpty()) + break; + + return CMStringA(FORMAT, "%s@%s", s1.c_str(), s2.c_str()); + } + + // error + return ""; +} + +CMStringA WAReader::readStringFromChars(int size) +{ + if (m_limit - m_buf < size) + return ""; + + CMStringA ret((char *)m_buf, size); + m_buf += size; + return ret; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// WAWriter class members + +void WAWriter::writeByte(uint8_t b) +{ + body.append(&b, 1); +} + +void WAWriter::writeIntN(int value, int n) +{ + for (int i = n - 1; i >= 0; i--) + writeByte((value >> i * 8) & 0xFF); +} + +void WAWriter::writeInt20(int value) +{ + writeByte((value >> 16) & 0xFF); + writeByte((value >> 8) & 0xFF); + writeByte(value & 0xFF); +} + +void WAWriter::writeLength(int value) +{ + if (value >= (1 << 20)) { + writeByte(BINARY_32); + writeInt32(value); + } + else if (value >= 256) { + writeByte(BINARY_20); + writeInt20(value); + } + else { + writeByte(BINARY_8); + writeInt8(value); + } +} + +void WAWriter::writeListSize(int length) +{ + if (length == 0) + writeByte(LIST_EMPTY); + else if (length < 256) { + writeByte(LIST_8); + writeInt8(length); + } + else { + writeByte(LIST_16); + writeInt16(length); + } +} + +void WAWriter::writeNode(const WANode *pNode) +{ + // we never send zipped content + if (pNode->pParent == nullptr) + writeByte(0); + + int numAttrs = (int)pNode->attrs.getCount(); + int hasContent = pNode->content.length() != 0 || pNode->children.getCount() != 0; + writeListSize(2 * numAttrs + 1 + hasContent); + + writeString(pNode->title.c_str()); + + // write attributes + for (auto &it : pNode->attrs) { + if (it->value.IsEmpty()) + continue; + + writeString(it->name.c_str()); + writeString(it->value.c_str()); + } + + // write contents + if (pNode->content.length()) { + writeLength((int)pNode->content.length()); + body.append(pNode->content.data(), pNode->content.length()); + } + // write children + else if (pNode->children.getCount()) { + writeListSize(pNode->children.getCount()); + for (auto &it : pNode->children) + writeNode(it); + } +} + +bool WAWriter::writeToken(const char *str) +{ + for (auto &it : SingleByteTokens) + if (!strcmp(str, it)) { + writeByte(int(&it - SingleByteTokens)); + return true; + } + + return false; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static BYTE packNibble(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + + switch (c) { + case '-': return 10; + case '.': return 11; + case 0: return 15; + } + + return -1; +} + +static BYTE packHex(char c) +{ + if (c == 0) + return 15; + + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'A' && c <= 'F') + return c - 'A'; + + if (c >= 'a' && c <= 'f') + return c - 'a'; + + return -1; +} + +static BYTE packPair(int type, char c1, char c2) +{ + BYTE b1 = (type == NIBBLE_8) ? packNibble(c1) : packHex(c1); + BYTE b2 = (type == NIBBLE_8) ? packNibble(c2) : packHex(c2); + return (b1 << 4) + b2; +} + +static bool isNibble(const CMStringA &str) +{ + return strspn(str, "0123456789-.") == str.GetLength(); +} + +static bool isHex(const CMStringA &str) +{ + return strspn(str, "0123456789abcdefABCDEF-.") == str.GetLength(); +} + +void WAWriter::writePacked(const CMStringA &str, int tag) +{ + if (str.GetLength() > 254) + return; + + writeByte(tag); + + int len = str.GetLength() / 2; + BYTE firstByte = (str.GetLength() % 2) == 0 ? 0 : 0x80; + writeByte(firstByte | len); + + const char *p = str; + for (int i = 0; i < len; i++, p += 2) + writeByte(packPair(tag, p[0], p[1])); + + if (firstByte != 0) + writeByte(packPair(tag, p[0], 0)); +} + +void WAWriter::writeString(const char *str) +{ + if (writeToken(str)) + return; + + auto *pszDelimiter = strchr(str, '@'); + if (pszDelimiter) { + writeByte(JID_PAIR); + + if (pszDelimiter == str) // empty jid + writeByte(LIST_EMPTY); + else + writeString(CMStringA(str, int(pszDelimiter - str))); + + if (pszDelimiter[1] == 0) // empty jid + writeByte(LIST_EMPTY); + else + writeString(pszDelimiter + 1); + } + else { + CMStringA buf(str); + if (isNibble(buf)) + writePacked(buf, NIBBLE_8); + else if (isHex(buf)) + writePacked(buf, HEX_8); + else { + writeLength(buf.GetLength()); + body.append(buf, buf.GetLength()); + } + } +} diff --git a/protocols/WhatsAppWeb/src/wareader.cpp b/protocols/WhatsAppWeb/src/wareader.cpp deleted file mode 100644 index 8a7ab3f99d..0000000000 --- a/protocols/WhatsAppWeb/src/wareader.cpp +++ /dev/null @@ -1,570 +0,0 @@ -/* - -WhatsAppWeb plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" -#include "dicts.h" - -struct Attr -{ - Attr(const char *pszName, const char *pszValue) : - name(pszName), - value(pszValue) - {} - - Attr(const char *pszName, int iValue) : - name(pszName), - value(FORMAT, "%d", iValue) - {} - - CMStringA name, value; -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// WANode members - -WANode::WANode() : - attrs(1), - children(1) -{} - -WANode::WANode(const char *pszTitle) : - attrs(1), - children(1), - title(pszTitle) -{} - -WANode::~WANode() -{ -} - -const char *WANode::getAttr(const char *pszName) const -{ - for (auto &p : attrs) - if (p->name == pszName) - return p->value.c_str(); - - return nullptr; -} - -void WANode::addAttr(const char *pszName, const char *pszValue) -{ - attrs.insert(new Attr(pszName, pszValue)); -} - -void WANode::addAttr(const char *pszName, int iValue) -{ - attrs.insert(new Attr(pszName, iValue)); -} - -CMStringA WANode::getBody() const -{ - return CMStringA((char *)content.data(), (int)content.length()); -} - -WANode *WANode::addChild(const char *pszName) -{ - auto *pNew = new WANode(pszName); - pNew->pParent = this; - children.insert(pNew); - return pNew; -} - -WANode* WANode::getChild(const char *pszName) const -{ - for (auto &it : children) - if (it->title == pszName) - return it; - - return nullptr; -} - -WANode *WANode::getFirstChild(void) const -{ - return (children.getCount()) ? &children[0] : nullptr; -} - -void WANode::print(CMStringA &dest, int level) const -{ - for (int i = 0; i < level; i++) - dest.Append(" "); - - dest.AppendFormat("<%s ", title.c_str()); - for (auto &p : attrs) - dest.AppendFormat("%s=\"%s\" ", p->name.c_str(), p->value.c_str()); - dest.Truncate(dest.GetLength() - 1); - - if (content.isEmpty() && !children.getCount()) { - dest.Append("/>\n"); - return; - } - - dest.Append(">"); - if (!content.isEmpty()) { - ptrA tmp((char *)mir_alloc(content.length() * 2 + 1)); - bin2hex(content.data(), content.length(), tmp); - dest.AppendFormat("%s", tmp.get()); - } - - if (children.getCount()) { - dest.Append("\n"); - - for (auto &p : children) - p->print(dest, level + 1); - - for (int i = 0; i < level; i++) - dest.Append(" "); - } - - dest.AppendFormat("\n", title.c_str()); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// WAReader class members - -bool WAReader::readAttributes(WANode *pNode, int count) -{ - if (count == 0) - return true; - - for (int i = 0; i < count; i++) { - CMStringA name = readString(readInt8()); - if (name.IsEmpty()) - return false; - - CMStringA value = readString(readInt8()); - if (value.IsEmpty()) - return false; - - pNode->addAttr(name, value); - } - return true; -} - -uint32_t WAReader::readInt20() -{ - if (m_limit - m_buf < 3) - return 0; - - int ret = (int(m_buf[0] & 0x0F) << 16) + (int(m_buf[1]) << 8) + int(m_buf[2]); - m_buf += 3; - return ret; -} - -uint32_t WAReader::readIntN(int n) -{ - if (m_limit - m_buf < n) - return 0; - - uint32_t res = 0; - for (int i = 0; i < n; i++, m_buf++) - res = (res <<= 8) + *m_buf; - return res; -} - -bool WAReader::readList(WANode *pParent, int tag) -{ - int size = readListSize(tag); - if (size == -1) - return false; - - for (int i = 0; i < size; i++) { - WANode *pNew = readNode(); - if (pNew == nullptr) - return false; - pParent->children.insert(pNew); - } - - return true; -} - -int WAReader::readListSize(int tag) -{ - switch (tag) { - case LIST_EMPTY: - return 0; - case LIST_8: - return readInt8(); - case LIST_16: - return readInt16(); - } - return -1; -} - -WANode *WAReader::readNode() -{ - int listSize = readListSize(readInt8()); - if (listSize == -1) - return nullptr; - - int descrTag = readInt8(); - if (descrTag == STREAM_END) - return nullptr; - - CMStringA name = readString(descrTag); - if (name.IsEmpty()) - return nullptr; - - std::unique_ptr ret(new WANode()); - ret->title = name.c_str(); - - if (!readAttributes(ret.get(), (listSize - 1) >> 1)) - return nullptr; - - if ((listSize % 2) == 1) - return ret.release(); - - int size, tag = readInt8(); - switch (tag) { - case LIST_EMPTY: case LIST_8: case LIST_16: - readList(ret.get(), tag); - break; - - case BINARY_8: - size = readInt8(); - -LBL_Binary: - if (m_limit - m_buf < size) - return false; - - ret->content.assign((void *)m_buf, size); - m_buf += size; - break; - - case BINARY_20: - size = readInt20(); - goto LBL_Binary; - - case BINARY_32: - size = readInt32(); - goto LBL_Binary; - - default: - CMStringA str = readString(tag); - ret->content.assign(str.GetBuffer(), str.GetLength() + 1); - } - - return ret.release(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static int unpackHex(int val) -{ - if (val < 0 || val > 15) - return -1; - - return (val < 10) ? val + '0' : val - 10 + 'A'; -} - -static int unpackNibble(int val) -{ - if (val < 0 || val > 15) - return -1; - - switch (val) { - case 10: return '-'; - case 11: return '.'; - case 15: return 0; - default: return '0' + val; - } -} - -CMStringA WAReader::readPacked(int tag) -{ - int startByte = readInt8(); - bool bTrim = false; - if (startByte & 0x80) { - startByte &= ~0x80; - bTrim = true; - } - - CMStringA ret; - for (int i = 0; i < startByte; i++) { - BYTE b = readInt8(); - int lower = (tag == NIBBLE_8) ? unpackNibble(b >> 4) : unpackHex(b >> 4); - if (lower == -1) - return ""; - - int higher = (tag == NIBBLE_8) ? unpackNibble(b & 0x0F) : unpackHex(b & 0x0F); - if (higher == -1) - return ""; - - ret.AppendChar(lower); - ret.AppendChar(higher); - } - - if (bTrim && !ret.IsEmpty()) - ret.Truncate(ret.GetLength() - 1); - return ret; -} - -CMStringA WAReader::readString(int tag) -{ - if (tag >= 1 && tag < _countof(SingleByteTokens)) - return SingleByteTokens[tag]; - - int idx; - switch (tag) { - case DICTIONARY_0: - idx = readInt8(); - return (idx < _countof(dict0)) ? dict0[idx] : ""; - - case DICTIONARY_1: - idx = readInt8(); - return (idx < _countof(dict1)) ? dict1[idx] : ""; - - case DICTIONARY_2: - idx = readInt8(); - return (idx < _countof(dict2)) ? dict2[idx] : ""; - - case DICTIONARY_3: - idx = readInt8(); - return (idx < _countof(dict3)) ? dict3[idx] : ""; - - case LIST_EMPTY: - return ""; - - case BINARY_8: - return readStringFromChars(readInt8()); - - case BINARY_20: - return readStringFromChars(readInt20()); - - case BINARY_32: - return readStringFromChars(readInt32()); - - case NIBBLE_8: - case HEX_8: - return readPacked(tag); - - case AD_JID: - { - int agent = readInt8(); - int device = readInt8(); - CMStringA user = readString(readInt8()); - return user + "@c.us"; - } - - case JID_PAIR: - CMStringA s1 = readString(readInt8()); - CMStringA s2 = readString(readInt8()); - if (s1.IsEmpty() && s2.IsEmpty()) - break; - - return CMStringA(FORMAT, "%s@%s", s1.c_str(), s2.c_str()); - } - - // error - return ""; -} - -CMStringA WAReader::readStringFromChars(int size) -{ - if (m_limit - m_buf < size) - return ""; - - CMStringA ret((char *)m_buf, size); - m_buf += size; - return ret; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// WAWriter class members - -void WAWriter::writeByte(uint8_t b) -{ - body.append(&b, 1); -} - -void WAWriter::writeIntN(int value, int n) -{ - for (int i = n - 1; i >= 0; i--) - writeByte((value >> i * 8) & 0xFF); -} - -void WAWriter::writeInt20(int value) -{ - writeByte((value >> 16) & 0xFF); - writeByte((value >> 8) & 0xFF); - writeByte(value & 0xFF); -} - -void WAWriter::writeLength(int value) -{ - if (value >= (1 << 20)) { - writeByte(BINARY_32); - writeInt32(value); - } - else if (value >= 256) { - writeByte(BINARY_20); - writeInt20(value); - } - else { - writeByte(BINARY_8); - writeInt8(value); - } -} - -void WAWriter::writeListSize(int length) -{ - if (length == 0) - writeByte(LIST_EMPTY); - else if (length < 256) { - writeByte(LIST_8); - writeInt8(length); - } - else { - writeByte(LIST_16); - writeInt16(length); - } -} - -void WAWriter::writeNode(const WANode *pNode) -{ - // we never send zipped content - if (pNode->pParent == nullptr) - writeByte(0); - - int numAttrs = (int)pNode->attrs.getCount(); - int hasContent = pNode->content.length() != 0 || pNode->children.getCount() != 0; - writeListSize(2 * numAttrs + 1 + hasContent); - - writeString(pNode->title.c_str()); - - // write attributes - for (auto &it : pNode->attrs) { - if (it->value.IsEmpty()) - continue; - - writeString(it->name.c_str()); - writeString(it->value.c_str()); - } - - // write contents - if (pNode->content.length()) { - writeLength((int)pNode->content.length()); - body.append(pNode->content.data(), pNode->content.length()); - } - // write children - else if (pNode->children.getCount()) { - writeListSize(pNode->children.getCount()); - for (auto &it : pNode->children) - writeNode(it); - } -} - -bool WAWriter::writeToken(const char *str) -{ - for (auto &it : SingleByteTokens) - if (!strcmp(str, it)) { - writeByte(int(&it - SingleByteTokens)); - return true; - } - - return false; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static BYTE packNibble(char c) -{ - if (c >= '0' && c <= '9') - return c - '0'; - - switch (c) { - case '-': return 10; - case '.': return 11; - case 0: return 15; - } - - return -1; -} - -static BYTE packHex(char c) -{ - if (c == 0) - return 15; - - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'A' && c <= 'F') - return c - 'A'; - - if (c >= 'a' && c <= 'f') - return c - 'a'; - - return -1; -} - -static BYTE packPair(int type, char c1, char c2) -{ - BYTE b1 = (type == NIBBLE_8) ? packNibble(c1) : packHex(c1); - BYTE b2 = (type == NIBBLE_8) ? packNibble(c2) : packHex(c2); - return (b1 << 4) + b2; -} - -static bool isNibble(const CMStringA &str) -{ - return strspn(str, "0123456789-.") == str.GetLength(); -} - -static bool isHex(const CMStringA &str) -{ - return strspn(str, "0123456789abcdefABCDEF-.") == str.GetLength(); -} - -void WAWriter::writePacked(const CMStringA &str, int tag) -{ - if (str.GetLength() > 254) - return; - - writeByte(tag); - - int len = str.GetLength() / 2; - BYTE firstByte = (str.GetLength() % 2) == 0 ? 0 : 0x80; - writeByte(firstByte | len); - - const char *p = str; - for (int i = 0; i < len; i++, p += 2) - writeByte(packPair(tag, p[0], p[1])); - - if (firstByte != 0) - writeByte(packPair(tag, p[0], 0)); -} - -void WAWriter::writeString(const char *str) -{ - if (writeToken(str)) - return; - - auto *pszDelimiter = strchr(str, '@'); - if (pszDelimiter) { - writeByte(JID_PAIR); - - if (pszDelimiter == str) // empty jid - writeByte(LIST_EMPTY); - else - writeString(CMStringA(str, int(pszDelimiter - str))); - - if (pszDelimiter[1] == 0) // empty jid - writeByte(LIST_EMPTY); - else - writeString(pszDelimiter + 1); - } - else { - CMStringA buf(str); - if (isNibble(buf)) - writePacked(buf, NIBBLE_8); - else if (isHex(buf)) - writePacked(buf, HEX_8); - else { - writeLength(buf.GetLength()); - body.append(buf, buf.GetLength()); - } - } -} -- cgit v1.2.3