// ---------------------------------------------------------------------------80 // ICQ plugin for Miranda Instant Messenger // ________________________________________ // // Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede // Copyright © 2001-2002 Jon Keating, Richard Hughes // Copyright © 2002-2004 Martin Цberg, Sam Kothari, Robert Rainwater // Copyright © 2004-2010 Joe Kucera // Copyright © 2012-2017 Miranda NG Team // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // ----------------------------------------------------------------------------- #include "stdafx.h" void CIcqProto::handleIcqExtensionsFam(BYTE *pBuffer, size_t wBufferLength, snac_header* pSnacHeader) { switch (pSnacHeader->wSubtype) { case ICQ_META_ERROR: handleExtensionError(pBuffer, wBufferLength); break; case ICQ_META_SRV_REPLY: handleExtensionServerInfo(pBuffer, wBufferLength, pSnacHeader->wFlags); break; default: debugLogA("Warning: Ignoring SNAC(x%02x,x%02x) - Unknown SNAC (Flags: %u, Ref: %u)", ICQ_EXTENSIONS_FAMILY, pSnacHeader->wSubtype, pSnacHeader->wFlags, pSnacHeader->dwRef); break; } } void CIcqProto::handleExtensionError(BYTE *buf, size_t wPackLen) { WORD wErrorCode; if (wPackLen < 2) wErrorCode = 0; if (wPackLen >= 2 && wPackLen <= 6) unpackWord(&buf, &wErrorCode); else { // TODO: cookies need to be handled and freed here on error unpackWord(&buf, &wErrorCode); wPackLen -= 2; oscar_tlv_chain *chain = readIntoTLVChain(&buf, wPackLen, 0); if (chain) { oscar_tlv* pTLV; pTLV = chain->getTLV(0x21, 1); // get meta error data if (pTLV && pTLV->wLen >= 8) { BYTE *pBuffer = pTLV->pData; WORD wData; pBuffer += 6; unpackLEWord(&pBuffer, &wData); // get request type switch (wData) { case CLI_META_INFO_REQ: if (pTLV->wLen >= 12) { WORD wSubType; WORD wCookie; unpackWord(&pBuffer, &wCookie); unpackLEWord(&pBuffer, &wSubType); // more sofisticated detection, send ack if (wSubType == META_REQUEST_FULL_INFO) { MCONTACT hContact; cookie_fam15_data *pCookieData = nullptr; int foundCookie = FindCookie(wCookie, &hContact, (void**)&pCookieData); if (foundCookie && pCookieData) { ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, (HANDLE)1, 0); ReleaseCookie(wCookie); // we do not leak cookie and memory } debugLogA("Full info request error 0x%02x received", wErrorCode); } else if (wSubType == META_SET_PASSWORD_REQ) { // failed to change user password, report to UI ProtoBroadcastAck(NULL, ACKTYPE_SETINFO, ACKRESULT_FAILED, (HANDLE)wCookie, 0); debugLogA("Meta change password request failed, error 0x%02x", wErrorCode); } else debugLogA("Meta request error 0x%02x received", wErrorCode); } else debugLogA("Meta request error 0x%02x received", wErrorCode); break; default: debugLogA("Unknown request 0x%02x error 0x%02x received", wData, wErrorCode); } disposeChain(&chain); return; } disposeChain(&chain); } } LogFamilyError(ICQ_EXTENSIONS_FAMILY, wErrorCode); } void CIcqProto::handleExtensionServerInfo(BYTE *buf, size_t wPackLen, WORD wFlags) { // The entire packet is encapsulated in a TLV type 1 oscar_tlv_chain *chain = readIntoTLVChain(&buf, wPackLen, 0); if (chain == nullptr) { debugLogA("Error: Broken snac 15/3 %d", 1); return; } oscar_tlv *dataTlv = chain->getTLV(0x0001, 1); if (dataTlv == nullptr) { disposeChain(&chain); debugLogA("Error: Broken snac 15/3 %d", 2); return; } BYTE *databuf = dataTlv->pData; wPackLen -= 4; _ASSERTE(dataTlv->wLen == wPackLen); _ASSERTE(wPackLen >= 10); if ((dataTlv->wLen == wPackLen) && (wPackLen >= 10)) { DWORD dwMyUin; WORD wBytesRemaining, wCookie, wRequestType; unpackLEWord(&databuf, &wBytesRemaining); unpackLEDWord(&databuf, &dwMyUin); unpackLEWord(&databuf, &wRequestType); unpackWord(&databuf, &wCookie); _ASSERTE(wBytesRemaining == (wPackLen - 2)); if (wBytesRemaining == (wPackLen - 2)) { wPackLen -= 10; switch (wRequestType) { case SRV_META_INFO_REPLY: // SRV_META request replies handleExtensionMetaResponse(databuf, wPackLen, wCookie, wFlags); break; default: debugLogA("Warning: Ignoring Meta response - Unknown type %d", wRequestType); break; } } } else debugLogA("Error: Broken snac 15/3 %d", 3); if (chain) disposeChain(&chain); } void CIcqProto::handleExtensionMetaResponse(BYTE *databuf, size_t wPacketLen, WORD wCookie, WORD wFlags) { WORD wReplySubtype; BYTE bResultCode; _ASSERTE(wPacketLen >= 3); if (wPacketLen >= 3) { // Reply subtype unpackLEWord(&databuf, &wReplySubtype); wPacketLen -= 2; // Success byte unpackByte(&databuf, &bResultCode); wPacketLen -= 1; switch (wReplySubtype) { case META_SET_PASSWORD_ACK: parseUserInfoUpdateAck(wCookie, wReplySubtype, bResultCode); break; case SRV_RANDOM_FOUND: case SRV_USER_FOUND: case SRV_LAST_USER_FOUND: parseSearchReplies(databuf, wPacketLen, wCookie, wReplySubtype, bResultCode); break; case META_PROCESSING_ERROR: // Meta processing error server reply // Todo: We only use this as an SMS ack, that will have to change { // Terminate buffer char *pszInfo = (char *)_alloca(wPacketLen + 1); if (wPacketLen > 0) memcpy(pszInfo, databuf, wPacketLen); pszInfo[wPacketLen] = 0; ProtoBroadcastAck(NULL, ICQACKTYPE_SMS, ACKRESULT_FAILED, (HANDLE)wCookie, (LPARAM)pszInfo); FreeCookie(wCookie); } break; case META_SMS_DELIVERY_RECEIPT: // Todo: This overlaps with META_SET_AFFINFO_ACK. // Todo: Check what happens if result != A if (wPacketLen > 8) { databuf += 6; // Some unknowns wPacketLen -= 6; size_t wNetworkNameLen, wAckLen; unpackWord(&databuf, &wNetworkNameLen); if (wPacketLen >= (wNetworkNameLen + 2)) { databuf += wNetworkNameLen; wPacketLen -= wNetworkNameLen; unpackWord(&databuf, &wAckLen); char *pszInfo = (char*)_alloca(wAckLen + 1); // Terminate buffer if (wAckLen > 0) memcpy(pszInfo, databuf, wAckLen); pszInfo[wAckLen] = 0; ProtoBroadcastAck(NULL, ICQACKTYPE_SMS, ACKRESULT_SENTREQUEST, (HANDLE)wCookie, (LPARAM)pszInfo); FreeCookie(wCookie); // Parsing success break; } } // Parsing failure debugLogA("Error: Failure parsing META_SMS_DELIVERY_RECEIPT"); break; case META_DIRECTORY_DATA: case META_DIRECTORY_RESPONSE: if (bResultCode == 0x0A) handleDirectoryQueryResponse(databuf, wPacketLen, wCookie, wReplySubtype, wFlags); else debugLogA("Error: Directory request failed, code %u", bResultCode); break; case META_DIRECTORY_UPDATE_ACK: if (bResultCode == 0x0A) handleDirectoryUpdateResponse(databuf, wPacketLen, wCookie); else debugLogA("Error: Directory request failed, code %u", bResultCode); break; case META_BASIC_USERINFO: case META_WORK_USERINFO: case META_MORE_USERINFO: case META_NOTES_USERINFO: case META_EMAIL_USERINFO: case META_INTERESTS_USERINFO: case META_AFFILATIONS_USERINFO: case META_SHORT_USERINFO: case META_HPAGECAT_USERINFO: debugLogA("Warning: Ignored 15/03 (legacy user info) replysubtype x%x", wReplySubtype); break; default: debugLogA("Warning: Ignored 15/03 replysubtype x%x", wReplySubtype); break; } // Success return; } // Failure debugLogA("Warning: Broken 15/03 ExtensionMetaResponse"); } void CIcqProto::ReleaseSearchCookie(DWORD dwCookie, cookie_search *pCookie) { if (pCookie) { FreeCookie(dwCookie); if (pCookie->dwMainId) { if (pCookie->dwStatus) { SAFE_FREE((void**)&pCookie); ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)dwCookie, 0); } else pCookie->dwStatus = 1; } else { SAFE_FREE((void**)&pCookie); ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)dwCookie, 0); } } else ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)dwCookie, 0); } void CIcqProto::parseSearchReplies(unsigned char *databuf, size_t wPacketLen, WORD wCookie, WORD wReplySubtype, BYTE bResultCode) { BYTE bParsingOK = FALSE; // For debugging purposes only BOOL bLastUser = FALSE; cookie_search *pCookie; if (!FindCookie(wCookie, nullptr, (void**)&pCookie)) { debugLogA("Warning: Received unexpected search reply"); pCookie = nullptr; } switch (wReplySubtype) { case SRV_LAST_USER_FOUND: // Search: last user found reply bLastUser = TRUE; case SRV_USER_FOUND: // Search: user found reply if (bLastUser) debugLogA("SNAC(0x15,0x3): Last search reply"); else debugLogA("SNAC(0x15,0x3): Search reply"); if (bResultCode == 0xA) { ICQSEARCHRESULT sr = { 0 }; DWORD dwUin; char szUin[UINMAXLEN]; size_t wLen; sr.hdr.cbSize = sizeof(sr); // Remaining bytes if (wPacketLen < 2) break; unpackLEWord(&databuf, &wLen); wPacketLen -= 2; _ASSERTE(wLen <= wPacketLen); if (wLen > wPacketLen) break; // Uin if (wPacketLen < 4) break; unpackLEDWord(&databuf, &dwUin); // Uin wPacketLen -= 4; sr.uin = dwUin; _itoa(dwUin, szUin, 10); sr.hdr.id.w = (wchar_t*)szUin; // Nick if (wPacketLen < 2) break; unpackLEWord(&databuf, &wLen); wPacketLen -= 2; if (wLen > 0) { if (wPacketLen < wLen || (databuf[wLen - 1] != 0)) break; sr.hdr.nick.w = (wchar_t*)databuf; databuf += wLen; } else { sr.hdr.nick.w = nullptr; } // First name if (wPacketLen < 2) break; unpackLEWord(&databuf, &wLen); wPacketLen -= 2; if (wLen > 0) { if (wPacketLen < wLen || (databuf[wLen - 1] != 0)) break; sr.hdr.firstName.w = (wchar_t*)databuf; databuf += wLen; } else sr.hdr.firstName.w = nullptr; // Last name if (wPacketLen < 2) break; unpackLEWord(&databuf, &wLen); wPacketLen -= 2; if (wLen > 0) { if (wPacketLen < wLen || (databuf[wLen - 1] != 0)) break; sr.hdr.lastName.w = (wchar_t*)databuf; databuf += wLen; } else sr.hdr.lastName.w = nullptr; // E-mail name if (wPacketLen < 2) break; unpackLEWord(&databuf, &wLen); wPacketLen -= 2; if (wLen > 0) { if (wPacketLen < wLen || (databuf[wLen - 1] != 0)) break; sr.hdr.email.w = (wchar_t*)databuf; databuf += wLen; } else sr.hdr.email.w = nullptr; // Authentication needed flag if (wPacketLen < 1) break; unpackByte(&databuf, &sr.auth); // Finally, broadcast the result ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)wCookie, (LPARAM)&sr); // Broadcast "Last result" ack if this was the last user found if (wReplySubtype == SRV_LAST_USER_FOUND) { if (wPacketLen >= 10) { DWORD dwLeft; databuf += 5; unpackLEDWord(&databuf, &dwLeft); if (dwLeft) debugLogA("Warning: %d search results omitted", dwLeft); } ReleaseSearchCookie(wCookie, pCookie); } bParsingOK = TRUE; } else { // Failed search debugLogA("SNAC(0x15,0x3): Search error %u", bResultCode); ReleaseSearchCookie(wCookie, pCookie); bParsingOK = TRUE; } break; case SRV_RANDOM_FOUND: // Random search server reply default: if (pCookie) ReleaseCookie(wCookie); break; } // For debugging purposes only if (!bParsingOK) { debugLogA("Warning: Parsing error in 15/03 search reply type x%x", wReplySubtype); _ASSERTE(!bParsingOK); } } void CIcqProto::parseUserInfoUpdateAck(WORD wCookie, WORD wReplySubtype, BYTE bResultCode) { switch (wReplySubtype) { case META_SET_PASSWORD_ACK: // Set user password server ack if (bResultCode == 0xA) ProtoBroadcastAck(NULL, ACKTYPE_SETINFO, ACKRESULT_SUCCESS, (HANDLE)wCookie, 0); else ProtoBroadcastAck(NULL, ACKTYPE_SETINFO, ACKRESULT_FAILED, (HANDLE)wCookie, 0); FreeCookie(wCookie); break; default: debugLogA("Warning: Ignored 15/03 user info update ack type x%x", wReplySubtype); break; } } UserInfoRecordItem rEmail[] = { { 0x64, DBVT_UTF8, "e-mail%u" } }; UserInfoRecordItem rAddress[] = { { 0x64, DBVT_UTF8, "Street" }, { 0x6E, DBVT_UTF8, "City" }, { 0x78, DBVT_UTF8, "State" }, { 0x82, DBVT_UTF8, "ZIP" }, { 0x8C, DBVT_WORD, "Country" } }; UserInfoRecordItem rOriginAddress[] = { { 0x64, DBVT_UTF8, "OriginStreet" }, { 0x6E, DBVT_UTF8, "OriginCity" }, { 0x78, DBVT_UTF8, "OriginState" }, { 0x8C, DBVT_WORD, "OriginCountry" } }; UserInfoRecordItem rCompany[] = { { 0x64, DBVT_UTF8, "CompanyPosition" }, { 0x6E, DBVT_UTF8, "Company" }, { 0x7D, DBVT_UTF8, "CompanyDepartment" }, { 0x78, DBVT_UTF8, "CompanyHomepage" }, { 0x82, DBVT_WORD, "CompanyIndustry" }, { 0xAA, DBVT_UTF8, "CompanyStreet" }, { 0xB4, DBVT_UTF8, "CompanyCity" }, { 0xBE, DBVT_UTF8, "CompanyState" }, { 0xC8, DBVT_UTF8, "CompanyZIP" }, { 0xD2, DBVT_WORD, "CompanyCountry" } }; UserInfoRecordItem rEducation[] = { { 0x64, DBVT_WORD, "StudyLevel" }, { 0x6E, DBVT_UTF8, "StudyInstitute" }, { 0x78, DBVT_UTF8, "StudyDegree" }, { 0x8C, DBVT_WORD, "StudyYear" } }; UserInfoRecordItem rInterest[] = { { 0x64, DBVT_UTF8, "Interest%uText" }, { 0x6E, DBVT_WORD, "Interest%uCat" } }; int CIcqProto::parseUserInfoRecord(MCONTACT hContact, oscar_tlv *pData, UserInfoRecordItem pRecordDef[], int nRecordDef, int nMaxRecords) { int nRecords = 0; if (pData && pData->wLen >= 2) { BYTE *pRecords = pData->pData; WORD wRecordCount; unpackWord(&pRecords, &wRecordCount); oscar_tlv_record_list *cData = readIntoTLVRecordList(&pRecords, pData->wLen - 2, nMaxRecords > wRecordCount ? wRecordCount : nMaxRecords); oscar_tlv_record_list *cDataItem = cData; while (cDataItem) { oscar_tlv_chain *cItem = cDataItem->item; for (int i = 0; i < nRecordDef; i++) { char szItemKey[MAX_PATH]; mir_snprintf(szItemKey, pRecordDef[i].szDbSetting, nRecords); switch (pRecordDef[i].dbType) { case DBVT_UTF8: writeDbInfoSettingTLVStringUtf(hContact, szItemKey, cItem, pRecordDef[i].wTLV); break; case DBVT_WORD: writeDbInfoSettingTLVWord(hContact, szItemKey, cItem, pRecordDef[i].wTLV); break; } } nRecords++; cDataItem = cDataItem->next; } // release memory disposeRecordList(&cData); } // remove old data from database if (!nRecords || nMaxRecords > 1) for (int i = nRecords; i <= nMaxRecords; i++) for (int j = 0; j < nRecordDef; j++) { char szItemKey[MAX_PATH]; mir_snprintf(szItemKey, pRecordDef[j].szDbSetting, i); delSetting(hContact, szItemKey); } return nRecords; } void CIcqProto::handleDirectoryQueryResponse(BYTE *databuf, size_t wPacketLen, WORD wCookie, WORD wReplySubtype, WORD wFlags) { WORD wBytesRemaining = 0; snac_header requestSnac = { 0 }; BYTE requestResult; debugLogA("Received directory query response"); if (wPacketLen >= 2) unpackLEWord(&databuf, &wBytesRemaining); wPacketLen -= 2; _ASSERTE(wPacketLen == wBytesRemaining); if (!unpackSnacHeader(&requestSnac, &databuf, &wPacketLen) || !requestSnac.bValid) { debugLogA("Error: Failed to parse directory response"); return; } cookie_directory_data *pCookieData; MCONTACT hContact; // check request cookie if (!FindCookie(wCookie, &hContact, (void**)&pCookieData) || !pCookieData) { debugLogA("Warning: Ignoring unrequested directory reply type (x%x, x%x)", requestSnac.wFamily, requestSnac.wSubtype); return; } /// FIXME: we should really check the snac contents according to cookie data here ?? // Check if this is the last packet for this request BOOL bMoreDataFollows = wFlags & 0x0001 && requestSnac.wFlags & 0x0001; if (wPacketLen >= 3) unpackByte(&databuf, &requestResult); else { debugLogA("Error: Malformed directory response"); if (!bMoreDataFollows) ReleaseCookie(wCookie); return; } if (requestResult != 1 && requestResult != 4) { debugLogA("Error: Directory request failed, status %u", requestResult); if (!bMoreDataFollows) { if (pCookieData->bRequestType == DIRECTORYREQUEST_INFOUSER) ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, (HANDLE)1, 0); else if (pCookieData->bRequestType == DIRECTORYREQUEST_SEARCH) ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)wCookie, 0); // should report error here, but Find/Add module does not support that ReleaseCookie(wCookie); } return; } size_t wLen; unpackWord(&databuf, &wLen); wPacketLen -= 3; if (wLen) debugLogA("Warning: Data in error message present!"); if (wPacketLen <= 0x16) { // sanity check debugLogA("Error: Malformed directory response"); if (!bMoreDataFollows) { if (pCookieData->bRequestType == DIRECTORYREQUEST_INFOUSER) ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, (HANDLE)1, 0); else if (pCookieData->bRequestType == DIRECTORYREQUEST_SEARCH) ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)wCookie, 0); // should report error here, but Find/Add module does not support that ReleaseCookie(wCookie); } return; } databuf += 0x10; // unknown stuff wPacketLen -= 0x10; DWORD dwItemCount; WORD wPageCount; /// FIXME: check itemcount, pagecount against the cookie data ??? unpackDWord(&databuf, &dwItemCount); unpackWord(&databuf, &wPageCount); wPacketLen -= 6; if (pCookieData->bRequestType == DIRECTORYREQUEST_SEARCH && !bMoreDataFollows) debugLogA("Directory Search: %d contacts found (%u pages)", dwItemCount, wPageCount); if (wPacketLen <= 2) { // sanity check, block expected debugLogA("Error: Malformed directory response"); if (!bMoreDataFollows) { if (pCookieData->bRequestType == DIRECTORYREQUEST_INFOUSER) ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, (HANDLE)1, 0); else if (pCookieData->bRequestType == DIRECTORYREQUEST_SEARCH) ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)wCookie, 0); // should report error here, but Find/Add module does not support that ReleaseCookie(wCookie); } return; } WORD wData; unpackWord(&databuf, &wData); // This probably the count of items following (a block) wPacketLen -= 2; if (wPacketLen >= 2 && wData >= 1) { unpackWord(&databuf, &wLen); // This is the size of the first item wPacketLen -= 2; } if (wData == 0 && pCookieData->bRequestType == DIRECTORYREQUEST_SEARCH) { debugLogA("Directory Search: No contacts found"); ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)wCookie, 0); ReleaseCookie(wCookie); return; } _ASSERTE(wData == 1 && wPacketLen == wLen); if (wData != 1 || wPacketLen != wLen) { debugLogA("Error: Malformed directory response (missing data)"); if (!bMoreDataFollows) { if (pCookieData->bRequestType == DIRECTORYREQUEST_INFOUSER) ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, (HANDLE)1, 0); else if (pCookieData->bRequestType == DIRECTORYREQUEST_SEARCH) ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)wCookie, 0); // should report error here, but Find/Add module does not support that ReleaseCookie(wCookie); } return; } oscar_tlv_chain *pDirectoryData = readIntoTLVChain(&databuf, wLen, -1); if (pDirectoryData) { switch (pCookieData->bRequestType) { case DIRECTORYREQUEST_INFOOWNER: parseDirectoryUserDetailsData(NULL, pDirectoryData, pCookieData, wReplySubtype); break; case DIRECTORYREQUEST_INFOUSER: { DWORD dwUin = 0; char *szUid = pDirectoryData->getString(0x32, 1); if (!szUid) { debugLogA("Error: Received unrecognized data from the directory"); break; } if (IsStringUIN(szUid)) dwUin = atoi(szUid); if (hContact != HContactFromUID(dwUin, szUid, nullptr)) { debugLogA("Error: Received data does not match cookie contact, ignoring."); SAFE_FREE(&szUid); break; } else SAFE_FREE(&szUid); } case DIRECTORYREQUEST_INFOMULTI: parseDirectoryUserDetailsData(hContact, pDirectoryData, pCookieData, wReplySubtype); break; case DIRECTORYREQUEST_SEARCH: parseDirectorySearchData(pDirectoryData, wCookie, wReplySubtype); break; default: debugLogA("Error: Unknown cookie type %x for directory response!", pCookieData->bRequestType); } disposeChain(&pDirectoryData); } else debugLogA("Error: Failed parsing directory response"); // Release Memory if (!bMoreDataFollows) ReleaseCookie(wCookie); } static int calcAgeFromBirthDate(double dDate) { if (dDate > 0) { // date is stored as double with unit equal to a day, incrementing since 1/1/1900 0:00 GMT SYSTEMTIME sDate = { 0 }; if (VariantTimeToSystemTime(dDate + 2, &sDate)) { SYSTEMTIME sToday = { 0 }; GetLocalTime(&sToday); int nAge = sToday.wYear - sDate.wYear; if (sToday.wMonth < sDate.wMonth || (sToday.wMonth == sDate.wMonth && sToday.wDay < sDate.wDay)) nAge--; return nAge; } } return 0; } void CIcqProto::parseDirectoryUserDetailsData(MCONTACT hContact, oscar_tlv_chain *cDetails, cookie_directory_data *pCookieData, WORD wReplySubType) { if (pCookieData->bRequestType == DIRECTORYREQUEST_INFOMULTI && !hContact) { DWORD dwUin = 0; char *szUid = cDetails->getString(0x32, 1); if (!szUid) { debugLogA("Error: Received unrecognized data from the directory"); return; } if (IsStringUIN(szUid)) dwUin = atoi(szUid); hContact = HContactFromUID(dwUin, szUid, nullptr); if (hContact == INVALID_CONTACT_ID) { debugLogA("Error: Received details for unknown contact \"%s\"", szUid); SAFE_FREE(&szUid); return; } debugLogA("Received user info for %s from directory", szUid); SAFE_FREE(&szUid); } oscar_tlv *pTLV = cDetails->getTLV(0x50, 1); if (pTLV && pTLV->wLen > 0) writeDbInfoSettingTLVStringUtf(hContact, "e-mail", cDetails, 0x50); // Verified e-mail else writeDbInfoSettingTLVStringUtf(hContact, "e-mail", cDetails, 0x55); // Pending e-mail writeDbInfoSettingTLVStringUtf(hContact, "FirstName", cDetails, 0x64); writeDbInfoSettingTLVStringUtf(hContact, "LastName", cDetails, 0x6E); writeDbInfoSettingTLVStringUtf(hContact, "Nick", cDetails, 0x78); // Home Address parseUserInfoRecord(hContact, cDetails->getTLV(0x96, 1), rAddress, _countof(rAddress), 1); // Origin Address parseUserInfoRecord(hContact, cDetails->getTLV(0xA0, 1), rOriginAddress, _countof(rOriginAddress), 1); // Phones pTLV = cDetails->getTLV(0xC8, 1); if (pTLV && pTLV->wLen >= 2) { BYTE *pRecords = pTLV->pData; WORD wRecordCount; unpackWord(&pRecords, &wRecordCount); oscar_tlv_record_list *cPhones = readIntoTLVRecordList(&pRecords, pTLV->wLen - 2, wRecordCount); if (cPhones) { oscar_tlv_chain *cPhone; cPhone = cPhones->getRecordByTLV(0x6E, 1); writeDbInfoSettingTLVStringUtf(hContact, "Phone", cPhone, 0x64); cPhone = cPhones->getRecordByTLV(0x6E, 2); writeDbInfoSettingTLVStringUtf(hContact, "CompanyPhone", cPhone, 0x64); cPhone = cPhones->getRecordByTLV(0x6E, 3); writeDbInfoSettingTLVStringUtf(hContact, "Cellular", cPhone, 0x64); cPhone = cPhones->getRecordByTLV(0x6E, 4); writeDbInfoSettingTLVStringUtf(hContact, "Fax", cPhone, 0x64); cPhone = cPhones->getRecordByTLV(0x6E, 5); writeDbInfoSettingTLVStringUtf(hContact, "CompanyFax", cPhone, 0x64); disposeRecordList(&cPhones); } else { // Remove old data when phones not available delSetting(hContact, "Phone"); delSetting(hContact, "CompanyPhone"); delSetting(hContact, "Cellular"); delSetting(hContact, "Fax"); delSetting(hContact, "CompanyFax"); } } else { // Remove old data when phones not available delSetting(hContact, "Phone"); delSetting(hContact, "CompanyPhone"); delSetting(hContact, "Cellular"); delSetting(hContact, "Fax"); delSetting(hContact, "CompanyFax"); } // Emails parseUserInfoRecord(hContact, cDetails->getTLV(0x8C, 1), rEmail, _countof(rEmail), 4); writeDbInfoSettingTLVByte(hContact, "Timezone", cDetails, 0x17C); // Company parseUserInfoRecord(hContact, cDetails->getTLV(0x118, 1), rCompany, _countof(rCompany), 1); // Education parseUserInfoRecord(hContact, cDetails->getTLV(0x10E, 1), rEducation, _countof(rEducation), 1); switch (cDetails->getNumber(0x82, 1)) { case 1: setByte(hContact, "Gender", 'F'); break; case 2: setByte(hContact, "Gender", 'M'); break; default: delSetting(hContact, "Gender"); } writeDbInfoSettingTLVStringUtf(hContact, "Homepage", cDetails, 0xFA); writeDbInfoSettingTLVDate(hContact, "BirthYear", "BirthMonth", "BirthDay", cDetails, 0x1A4); writeDbInfoSettingTLVByte(hContact, "Language1", cDetails, 0xAA); writeDbInfoSettingTLVByte(hContact, "Language2", cDetails, 0xB4); writeDbInfoSettingTLVByte(hContact, "Language3", cDetails, 0xBE); writeDbInfoSettingTLVByte(hContact, "MaritalStatus", cDetails, 0x12C); // Interests parseUserInfoRecord(hContact, cDetails->getTLV(0x122, 1), rInterest, _countof(rInterest), 4); writeDbInfoSettingTLVStringUtf(hContact, "About", cDetails, 0x186); // if (hContact) // writeDbInfoSettingTLVStringUtf(hContact, DBSETTING_STATUS_NOTE, cDetails, 0x226); // else if (!hContact) { // Owner contact needs special processing, in the database is current status note for the client // We just received the last status note set on directory, if it differs call SetStatusNote() to // ensure the directory will be updated (it should be in process anyway) char *szClientStatusNote = getSettingStringUtf(hContact, DBSETTING_STATUS_NOTE, nullptr); char *szDirectoryStatusNote = cDetails->getString(0x226, 1); if (mir_strcmp(szClientStatusNote, szDirectoryStatusNote)) SetStatusNote(szClientStatusNote, 1000, TRUE); // Release memory SAFE_FREE(&szDirectoryStatusNote); SAFE_FREE(&szClientStatusNote); } writeDbInfoSettingTLVByte(hContact, "PrivacyLevel", cDetails, 0x1F9); if (!hContact) { setByte(hContact, "Auth", !cDetails->getByte(0x19A, 1)); writeDbInfoSettingTLVByte(hContact, "WebAware", cDetails, 0x212); writeDbInfoSettingTLVByte(hContact, "AllowSpam", cDetails, 0x1EA); } writeDbInfoSettingTLVWord(hContact, "InfoCP", cDetails, 0x1C2); if (hContact) { // Handle deprecated setting (Age & Birthdate are not separate fields anymore) int nAge = calcAgeFromBirthDate(cDetails->getDouble(0x1A4, 1)); if (nAge) setWord(hContact, "Age", nAge); else delSetting(hContact, "Age"); } else // we do not need to calculate age for owner delSetting(hContact, "Age"); { // Save user info last update time and privacy token double dInfoTime; BYTE pbEmptyMetaToken[0x10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; int bHasMetaToken = FALSE; // Check if the details arrived with privacy token! if ((pTLV = cDetails->getTLV(0x3C, 1)) && pTLV->wLen == 0x10 && memcmp(pTLV->pData, pbEmptyMetaToken, 0x10)) bHasMetaToken = TRUE; // !Important, we need to save the MDir server-item time - it can be newer than the one from the directory if ((dInfoTime = getSettingDouble(hContact, DBSETTING_METAINFO_TIME, 0)) > 0) setSettingDouble(hContact, DBSETTING_METAINFO_SAVED, dInfoTime); else if (bHasMetaToken || !hContact) writeDbInfoSettingTLVDouble(hContact, DBSETTING_METAINFO_SAVED, cDetails, 0x1CC); else setDword(hContact, DBSETTING_METAINFO_SAVED, time(nullptr)); } if (wReplySubType == META_DIRECTORY_RESPONSE) if (pCookieData->bRequestType == DIRECTORYREQUEST_INFOUSER) ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, (HANDLE)1, 0); // Remove user from info update queue. Removing is fast so we always call this // even if it is likely that the user is not queued at all. if (hContact) icq_DequeueUser(getContactUin(hContact)); } void CIcqProto::parseDirectorySearchData(oscar_tlv_chain *cDetails, DWORD dwCookie, WORD wReplySubType) { char *szUid = cDetails->getString(0x32, 1); // User ID debugLogA("Directory Search: Found user %s", szUid); ICQSEARCHRESULT isr = { 0 }; isr.hdr.cbSize = sizeof(ICQSEARCHRESULT); isr.hdr.flags = PSR_UTF8; isr.hdr.id.a = szUid; if (IsStringUIN(szUid)) isr.uin = atoi(szUid); else isr.uin = 0; oscar_tlv *pTLV = cDetails->getTLV(0x50, 1); char *szData = nullptr; if (pTLV && pTLV->wLen > 0) szData = cDetails->getString(0x50, 1); // Verified e-mail else szData = cDetails->getString(0x55, 1); // Pending e-mail if (szData != nullptr) isr.hdr.email.a = szData; szData = cDetails->getString(0x64, 1); // First Name if (szData != nullptr) isr.hdr.firstName.a = szData; szData = cDetails->getString(0x6E, 1); // Last Name if (szData != nullptr) isr.hdr.lastName.a = szData; szData = cDetails->getString(0x78, 1); // Nick if (szData != nullptr) isr.hdr.nick.a = szData; switch (cDetails->getNumber(0x82, 1)) { // Gender case 1: isr.gender = 'F'; break; case 2: isr.gender = 'M'; break; } pTLV = cDetails->getTLV(0x96, 1); if (pTLV && pTLV->wLen >= 4) { BYTE *buf = pTLV->pData; oscar_tlv_chain *chain = readIntoTLVChain(&buf, pTLV->wLen, 0); if (chain) isr.country = chain->getDWord(0x8C, 1); // Home Country disposeChain(&chain); } isr.auth = !cDetails->getByte(0x19A, 1); // Require Authorization isr.maritalStatus = cDetails->getNumber(0x12C, 1); // Marital Status // calculate Age if Birthdate is available isr.age = calcAgeFromBirthDate(cDetails->getDouble(0x1A4, 1)); // Finally, broadcast the result ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)dwCookie, (LPARAM)&isr); // Release memory SAFE_FREE(&isr.hdr.id.a); SAFE_FREE(&isr.hdr.nick.a); SAFE_FREE(&isr.hdr.firstName.a); SAFE_FREE(&isr.hdr.lastName.a); SAFE_FREE(&isr.hdr.email.a); // Search is over, broadcast final ack if (wReplySubType == META_DIRECTORY_RESPONSE) ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)dwCookie, 0); } void CIcqProto::handleDirectoryUpdateResponse(BYTE *databuf, size_t wPacketLen, WORD wCookie) { WORD wBytesRemaining = 0; snac_header requestSnac = { 0 }; BYTE requestResult; debugLogA("Received directory update response"); if (wPacketLen >= 2) unpackLEWord(&databuf, &wBytesRemaining); wPacketLen -= 2; _ASSERTE(wPacketLen == wBytesRemaining); if (!unpackSnacHeader(&requestSnac, &databuf, &wPacketLen) || !requestSnac.bValid) { debugLogA("Error: Failed to parse directory response"); return; } cookie_directory_data *pCookieData; MCONTACT hContact; // check request cookie if (!FindCookie(wCookie, &hContact, (void**)&pCookieData) || !pCookieData) { debugLogA("Warning: Ignoring unrequested directory reply type (x%x, x%x)", requestSnac.wFamily, requestSnac.wSubtype); return; } /// FIXME: we should really check the snac contents according to cookie data here ?? if (wPacketLen >= 3) unpackByte(&databuf, &requestResult); else { debugLogA("Error: Malformed directory response"); ReleaseCookie(wCookie); return; } if (requestResult != 1 && requestResult != 4) { debugLogA("Error: Directory request failed, status %u", requestResult); if (pCookieData->bRequestType == DIRECTORYREQUEST_UPDATEOWNER) ProtoBroadcastAck(NULL, ACKTYPE_SETINFO, ACKRESULT_FAILED, (HANDLE)wCookie, 0); ReleaseCookie(wCookie); return; } size_t wLen; unpackWord(&databuf, &wLen); wPacketLen -= 3; if (wLen) debugLogA("Warning: Data in error message present!"); if (pCookieData->bRequestType == DIRECTORYREQUEST_UPDATEOWNER) ProtoBroadcastAck(NULL, ACKTYPE_SETINFO, ACKRESULT_SUCCESS, (HANDLE)wCookie, 0); if (wPacketLen == 0x18) { DWORD64 qwMetaTime; BYTE pbEmptyMetaToken[0x10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; unpackQWord(&databuf, &qwMetaTime); setSettingBlob(NULL, DBSETTING_METAINFO_TIME, (BYTE*)&qwMetaTime, 8); if (memcmp(databuf, pbEmptyMetaToken, 0x10)) setSettingBlob(NULL, DBSETTING_METAINFO_TOKEN, databuf, 0x10); } ReleaseCookie(wCookie); }