diff options
-rw-r--r-- | protocols/WhatsAppWeb/src/proto.cpp | 43 | ||||
-rw-r--r-- | protocols/WhatsAppWeb/src/proto.h | 18 | ||||
-rw-r--r-- | protocols/WhatsAppWeb/src/server.cpp | 115 | ||||
-rw-r--r-- | protocols/WhatsAppWeb/src/utils.cpp | 180 | ||||
-rw-r--r-- | protocols/WhatsAppWeb/src/utils.h | 38 |
5 files changed, 367 insertions, 27 deletions
diff --git a/protocols/WhatsAppWeb/src/proto.cpp b/protocols/WhatsAppWeb/src/proto.cpp index d1117846aa..aab94609fe 100644 --- a/protocols/WhatsAppWeb/src/proto.cpp +++ b/protocols/WhatsAppWeb/src/proto.cpp @@ -27,6 +27,7 @@ WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) : m_impl(*this), m_tszDefaultGroup(getWStringA(DBKEY_DEF_GROUP)), m_arUsers(10, CompareUsers), + m_arOwnMsgs(1, NumericKeySortT), m_arPacketQueue(10, NumericKeySortT), m_wszDefaultGroup(this, "DefaultGroup", L"WhatsApp"), m_bHideGroupchats(this, "HideChats", true) @@ -191,6 +192,32 @@ int WhatsAppProto::SetStatus(int new_status) return 0; } +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnSendMessage(const JSONNode &node) +{ + int pktId = node["$id$"].as_int(); + + WAOwnMessage tmp(pktId, 0); + { + mir_cslock lck(m_csOwnMessages); + auto *pOwn = m_arOwnMsgs.find(&tmp); + if (pOwn == nullptr) + return; + + tmp.hContact = pOwn->hContact; + m_arOwnMsgs.remove(pOwn); + } + + int status = node["status"].as_int(); + if (status == 200) + ProtoBroadcastAck(tmp.hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)pktId); + else { + CMStringW wszError(FORMAT, TranslateT("Operation failed with server error status %d"), status); + ProtoBroadcastAck(tmp.hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)pktId, LPARAM(wszError.c_str())); + } +} + int WhatsAppProto::SendMsg(MCONTACT hContact, int, const char *pszMsg) { ptrA jid(getStringA(hContact, DBKEY_ID)); @@ -214,7 +241,21 @@ int WhatsAppProto::SendMsg(MCONTACT hContact, int, const char *pszMsg) size_t cbBinaryLen = msg.ByteSizeLong(); mir_ptr<BYTE> pBuf((BYTE *)mir_alloc(cbBinaryLen)); msg.SerializeToArray(pBuf, (int)cbBinaryLen); - return 0; + + auto *message = new WANode(); + message->title = "message"; + message->content.assign(pBuf, cbBinaryLen); + + WANode payLoad; + payLoad.title = "action"; + payLoad.addAttr("type", "relay"); + payLoad.children.push_back(message); + + int pktId = WSSendNode(payLoad, &WhatsAppProto::OnSendMessage); + + mir_cslock lck(m_csOwnMessages); + m_arOwnMsgs.insert(new WAOwnMessage(pktId, hContact)); + return pktId; } int WhatsAppProto::UserIsTyping(MCONTACT hContact, int) diff --git a/protocols/WhatsAppWeb/src/proto.h b/protocols/WhatsAppWeb/src/proto.h index 16ce1bb30f..d7a3723e89 100644 --- a/protocols/WhatsAppWeb/src/proto.h +++ b/protocols/WhatsAppWeb/src/proto.h @@ -38,6 +38,17 @@ struct WAUser DWORD m_time1 = 0, m_time2 = 0; }; +struct WAOwnMessage +{ + WAOwnMessage(int _1, MCONTACT _2) : + pktId(_1), + hContact(_2) + {} + + int pktId; + MCONTACT hContact; +}; + class WhatsAppProto : public PROTO<WhatsAppProto> { class CWhatsAppProtoImpl @@ -74,6 +85,9 @@ class WhatsAppProto : public PROTO<WhatsAppProto> mir_cs m_csUsers; OBJLIST<WAUser> m_arUsers; + mir_cs m_csOwnMessages; + OBJLIST<WAOwnMessage> m_arOwnMsgs; + WAUser* FindUser(const char *szId); WAUser* AddUser(const char *szId, bool bTemporary); @@ -97,6 +111,7 @@ class WhatsAppProto : public PROTO<WhatsAppProto> bool WSReadPacket(const WSHeader &hdr, MBinBuffer &buf); int WSSend(const CMStringA &str, WA_PKT_HANDLER = nullptr); + int WSSendNode(WANode &node, WA_PKT_HANDLER = nullptr); void OnLoggedIn(void); void OnLoggedOut(void); @@ -114,10 +129,11 @@ class WhatsAppProto : public PROTO<WhatsAppProto> void OnRestoreSession1(const JSONNode &node); void OnRestoreSession2(const JSONNode &node); + void OnSendMessage(const JSONNode &node); void OnStartSession(const JSONNode &node); // binary packets - void ProcessBinaryPacket(const MBinBuffer &buf); + void ProcessBinaryPacket(const void *pData, size_t cbLen); void ProcessAdd(const CMStringA &type, const WANode *node); void ProcessChats(const WANode *node); void ProcessContacts(const WANode *node); diff --git a/protocols/WhatsAppWeb/src/server.cpp b/protocols/WhatsAppWeb/src/server.cpp index 60eb227c97..c89926d1ce 100644 --- a/protocols/WhatsAppWeb/src/server.cpp +++ b/protocols/WhatsAppWeb/src/server.cpp @@ -35,10 +35,87 @@ int WhatsAppProto::WSSend(const CMStringA &str, WA_PKT_HANDLER pHandler) } debugLogA("Sending packet #%d: %s", pktId, buf.c_str()); - WebSocket_Send(m_hServerConn, buf.c_str(), buf.GetLength()); + WebSocket_SendText(m_hServerConn, buf.c_str()); return pktId; } +///////////////////////////////////////////////////////////////////////////////////////// + +static char zeroData[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER pHandler) +{ + if (m_hServerConn == nullptr) + return 0; + + { + char str[100]; + _i64toa(_time64(0), str, 10); + node.addAttr("epoch", str); + + CMStringA szText; + node.print(szText); + debugLogA("Sending binary node: %s", szText.c_str()); + } + + WAWriter writer; + writer.writeNode(&node); + + // AES block size = 16 bytes, let's expand data to block size boundary + size_t rest = writer.body.length() % 16; + if (rest != 0) + writer.body.append(zeroData, 16 - rest); + + BYTE iv[16]; + Utils_GetRandom(iv, sizeof(iv)); + + // allocate the buffer of the same size + 32 bytes for temporary operations + MBinBuffer enc; + enc.assign(writer.body.data(), writer.body.length()); + enc.append(mac_key.data(), mac_key.length()); + + int dec_len = 0, final_len = 0; + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, (BYTE*)enc_key.data(), iv); + EVP_EncryptUpdate(ctx, (BYTE*)enc.data(), &dec_len, (BYTE*)writer.body.data(), (int)writer.body.length()); + EVP_EncryptFinal_ex(ctx, (BYTE*)enc.data() + dec_len, &final_len); + EVP_CIPHER_CTX_free(ctx); + + // build the resulting buffer of the following structure: + // - packet prefix + // - 32 bytes of HMAC + // - 16 bytes of iv + // - rest of encoded data + + BYTE hmac[32]; + unsigned int hmac_len = 32; + HMAC(EVP_sha256(), mac_key.data(), (int)mac_key.length(), (BYTE*)enc.data(), (int)enc.length(), hmac, &hmac_len); + + int pktId = ++m_iPktNumber; + CMStringA prefix(FORMAT, "%d.--%d,", (int)m_iLoginTime, pktId); + + if (pHandler != nullptr) { + auto *pReq = new WARequest; + pReq->issued = time(0); + pReq->pHandler = pHandler; + pReq->pktId = pktId; + + mir_cslock lck(m_csPacketQueue); + m_arPacketQueue.insert(pReq); + } + + MBinBuffer ret; + ret.append(prefix, prefix.GetLength()); + ret.append(hmac, sizeof(hmac)); + ret.append(iv, sizeof(iv)); + ret.append(enc.data(), enc.length()); + WebSocket_SendBinary(m_hServerConn, ret.data(), ret.length()); + + return pktId; +} + +///////////////////////////////////////////////////////////////////////////////////////// + void WhatsAppProto::OnLoggedIn() { debugLogA("CDiscordProto::OnLoggedIn"); @@ -64,7 +141,7 @@ void WhatsAppProto::OnLoggedOut(void) void WhatsAppProto::SendKeepAlive() { - WebSocket_Send(m_hServerConn, "?,,", 3); + WebSocket_SendText(m_hServerConn, "?,,"); time_t now = time(0); @@ -362,28 +439,20 @@ bool WhatsAppProto::ServerThreadWorker() size_t dataSize = hdr.payloadSize - size_t(pos - start); // try to decode - if (hdr.opCode == 2 && hdr.payloadSize > 32) { - MBinBuffer dest; - if (!decryptBinaryMessage(dataSize, pos, dest)) { - Netlib_Dump(m_hServerConn, currPacket.data(), currPacket.length(), false, 0); - debugLogA("cannot decrypt incoming message"); - break; - } - - // Netlib_Dump(m_hServerConn, dest.data(), dest.length(), false, 0); - ProcessBinaryPacket(dest); - } + if (hdr.opCode == 2 && hdr.payloadSize > 32) + ProcessBinaryPacket(pos, dataSize); else { CMStringA szJson(pos, (int)dataSize); JSONNode root = JSONNode::parse(szJson); if (root) { - debugLogA("JSON received:\n%s", szJson.c_str()); + debugLogA("JSON received:\n%s", start); int sessId, pktId; if (sscanf(start, "%d.--%d,", &sessId, &pktId) == 2) { auto *pReq = m_arPacketQueue.find((WARequest *)&pktId); if (pReq != nullptr) { + root << INT_PARAM("$id$", pktId); (this->*pReq->pHandler)(root); } } @@ -433,16 +502,26 @@ bool WhatsAppProto::ServerThreadWorker() ///////////////////////////////////////////////////////////////////////////////////////// // Binary data processing -void WhatsAppProto::ProcessBinaryPacket(const MBinBuffer &buf) +void WhatsAppProto::ProcessBinaryPacket(const void *pData, size_t cbDataLen) { - WAReader reader(buf.data(), buf.length()); + MBinBuffer dest; + if (!decryptBinaryMessage(cbDataLen, pData, dest)) { + debugLogA("cannot decrypt incoming message"); + Netlib_Dump(m_hServerConn, pData, cbDataLen, false, 0); + return; + } + + WAReader reader(dest.data(), dest.length()); WANode *pRoot = reader.readNode(); - if (pRoot == nullptr) // smth went wrong + if (pRoot == nullptr) { // smth went wrong + debugLogA("cannot read binary message"); + Netlib_Dump(m_hServerConn, dest.data(), dest.length(), false, 0); return; + } CMStringA szText; pRoot->print(szText); - debugLogA("packed JSON: %s", szText.c_str()); + debugLogA("packed content: %s", szText.c_str()); CMStringA szType = pRoot->getAttr("type"); if (szType == "contacts") diff --git a/protocols/WhatsAppWeb/src/utils.cpp b/protocols/WhatsAppWeb/src/utils.cpp index 4631b32fcd..853c5ae383 100644 --- a/protocols/WhatsAppWeb/src/utils.cpp +++ b/protocols/WhatsAppWeb/src/utils.cpp @@ -148,6 +148,7 @@ void WANode::print(CMStringA &dest, int level) const } ///////////////////////////////////////////////////////////////////////////////////////// +// WAReader class members bool WAReader::readAttributes(WANode *pNode, int count) { @@ -411,3 +412,182 @@ CMStringA WAReader::readStringFromChars(int 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) +{ + int numAttrs = (int)pNode->attrs.size(); + int hasContent = pNode->content.length() != 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 + if (pNode->children.size()) { + writeListSize((int)pNode->children.size()); + for (auto &it : pNode->children) + writeNode(it); + } +} + +void WAWriter::writeString(const char *str, bool bRaw) +{ + if (!bRaw && !mir_strcmp(str, "c.us")) { + writeToken("s.whatsapp.net"); + return; + } + + if (writeToken(str)) + return; + + auto *pszDelimiter = strchr(str, '@'); + if (pszDelimiter) { + writeByte(JID_PAIR); + + if (pszDelimiter == str) // empty jid + writeByte(LIST_EMPTY); + else + writePacked(CMStringA(str, int(pszDelimiter - str))); + } + else { + int len = (int)strlen(str); + writeLength(len); + body.append((void *)str, len); + } +} + +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; +} + +void WAWriter::writePacked(const CMStringA &str) +{ + if (str.GetLength() > 254) + return; + + // all symbols of str can be a nibble? + int type = (strspn(str, "0123456789-.") == str.GetLength()) ? NIBBLE_8 : HEX_8; + + int len = str.GetLength() / 2; + BYTE firstByte = (len % 2) == 0 ? 0 : 0x80; + writeByte(firstByte | len); + + const char *p = str; + for (int i = 0; i < len; i++, p += 2) + writeByte(packPair(type, p[0], p[1])); + + if (firstByte != 0) + writeByte(packPair(type, p[0], 0)); +} diff --git a/protocols/WhatsAppWeb/src/utils.h b/protocols/WhatsAppWeb/src/utils.h index c2a5d90ca3..5ed3a451e1 100644 --- a/protocols/WhatsAppWeb/src/utils.h +++ b/protocols/WhatsAppWeb/src/utils.h @@ -22,6 +22,8 @@ Copyright © 2019-21 George Hazan class WANode // kinda XML { + friend class WAWriter; + struct Attr { Attr(const char *pszName, const char *pszValue) : @@ -55,12 +57,6 @@ class WAReader uint32_t readIntN(int i); CMStringA readStringFromChars(int size); -public: - WAReader(const void *buf, size_t cbLen) : - m_buf((BYTE*)buf), - m_limit((BYTE*)buf + cbLen) - {} - __forceinline uint32_t readInt8() { return readIntN(1); } __forceinline uint32_t readInt16() { return readIntN(2); } __forceinline uint32_t readInt32() { return readIntN(4); } @@ -71,5 +67,33 @@ public: int readListSize(int tag); CMStringA readPacked(int tag); CMStringA readString(int tag); - WANode* readNode(); + +public: + WAReader(const void *buf, size_t cbLen) : + m_buf((BYTE*)buf), + m_limit((BYTE*)buf + cbLen) + {} + + WANode* readNode(); +}; + +class WAWriter +{ + __forceinline void writeInt8(int value) { writeIntN(value, 1); } + __forceinline void writeInt16(int value) { writeIntN(value, 2); } + __forceinline void writeInt32(int value) { writeIntN(value, 4); } + + void writeByte(uint8_t b); + void writeIntN(int value, int i); + void writeInt20(int value); + void writeLength(int value); + void writeListSize(int tag); + void writePacked(const CMStringA &str); + void writeString(const char *str, bool bRaw = false); + bool writeToken(const char *str); + +public: + void writeNode(const WANode *pNode); + + MBinBuffer body; }; |