/* Copyright (C) 2012-25 Miranda NG team (https://miranda-ng.org) 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 version 2 of the License. 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, see . */ #include "stdafx.h" void __cdecl CSteamProto::ServerThread(void *) { // load web socket servers first if needed int iTimeDiff = db_get_dw(0, MODULENAME, DBKEY_HOSTS_DATE); int iHostCount = db_get_dw(0, MODULENAME, DBKEY_HOSTS_COUNT); if (!iHostCount || time(0) - iTimeDiff > 3600 * 24 * 7) { // once a week if (!SendRequest(new GetHostsRequest(), &CSteamProto::OnGotHosts)) { Logout(); return; } iHostCount = db_get_dw(0, MODULENAME, DBKEY_HOSTS_COUNT); } srand(time(0)); m_ws = nullptr; CMStringA szHost; szHost.Format("Host%d", rand() % iHostCount); szHost = db_get_sm(0, MODULENAME, szHost); szHost.Insert(0, "wss://"); szHost += "/cmsocket/"; WebSocket ws(this); NLHR_PTR pReply(ws.connect(m_hNetlibUser, szHost)); if (pReply) { if (pReply->resultCode == 101) { m_ws = &ws; debugLogA("Websocket connection succeeded"); // Send init packets Login(); ws.run(); } else debugLogA("websocket connection failed: %d", pReply->resultCode); } else debugLogA("websocket connection failed"); Logout(); m_impl.m_heartBeat.Stop(); m_ws = nullptr; } ///////////////////////////////////////////////////////////////////////////////////////// void WebSocket::process(const uint8_t *buf, size_t cbLen) { p->ProcessMessage(buf, cbLen); } void CSteamProto::ProcessMulti(const uint8_t *buf, size_t cbLen) { proto::MsgMulti pMulti(buf, cbLen); if (pMulti == nullptr) { debugLogA("Unable to decode multi message, exiting"); return; } debugLogA("processing %s multi message of size %d", (pMulti->size_unzipped) ? "zipped" : "normal", pMulti->message_body.len); ptrA tmp; if (pMulti->size_unzipped) { tmp = (char *)mir_alloc(pMulti->size_unzipped + 1); cbLen = FreeImage_ZLibGUnzip((uint8_t*)tmp.get(), pMulti->size_unzipped, pMulti->message_body.data, (unsigned)pMulti->message_body.len); if (!cbLen) { debugLogA("Unable to unzip multi message, exiting"); return; } buf = (const uint8_t *)tmp.get(); } else { buf = pMulti->message_body.data; cbLen = pMulti->message_body.len; } while ((int)cbLen > 0) { uint32_t cbPacketLen = *(uint32_t *)buf; buf += sizeof(uint32_t); cbLen -= sizeof(uint32_t); ProcessMessage(buf, cbPacketLen); buf += cbPacketLen; cbLen -= cbPacketLen; } } void CSteamProto::ProcessMessage(const uint8_t *buf, size_t cbLen) { ptrA szTargetJobName; CMsgProtoBufHeader hdr; uint32_t dwSign = *(uint32_t *)buf; buf += sizeof(uint32_t); cbLen -= sizeof(uint32_t); EMsg msgType = (EMsg)(dwSign & ~STEAM_PROTOCOL_MASK); bool bIsProtobuf = (dwSign & STEAM_PROTOCOL_MASK) != 0; if (msgType == EMsg::ChannelEncryptRequest || msgType == EMsg::ChannelEncryptResult) { hdr.has_jobid_source = hdr.has_jobid_target = true; hdr.jobid_source = *(int64_t *)buf; buf += sizeof(int64_t); hdr.jobid_target = *(int64_t *)buf; buf += sizeof(int64_t); debugLogA("Encrypted results cannot be processed, ignoring"); return; } if (msgType == EMsg::Multi) { buf += sizeof(uint32_t); cbLen -= sizeof(uint32_t); ProcessMulti(buf, cbLen); return; } if (bIsProtobuf) { uint32_t hdrLen = *(uint32_t *)buf; buf += sizeof(uint32_t); cbLen -= sizeof(uint32_t); auto *p = cmsg_proto_buf_header__unpack(0, hdrLen, buf); if (p == nullptr) { debugLogA("Unable to decode message header, exiting"); return; } buf += hdrLen; cbLen -= hdrLen; memcpy(&hdr, p, sizeof(hdr)); if (hdr.target_job_name) { szTargetJobName = mir_strdup(hdr.target_job_name); hdr.target_job_name = szTargetJobName; } cmsg_proto_buf_header__free_unpacked(p, 0); if (hdr.has_client_sessionid) m_iSessionId = hdr.client_sessionid; } else { // non protobuf message buf += 3; // 1 byte for header size (fixed at 36), 2 bytes for header version (fixed at 2) hdr.jobid_target = *(uint64_t *)buf; buf += sizeof(uint64_t); hdr.jobid_source = *(uint64_t *)buf; buf += sizeof(uint64_t); buf++; // 1 byte for header canary (fixed at 239) hdr.steamid = *(uint64_t *)buf; buf += sizeof(uint64_t); hdr.client_sessionid = *(uint32_t *)buf; buf += sizeof(uint32_t); hdr.has_jobid_target = hdr.has_jobid_source = hdr.has_steamid = hdr.has_client_sessionid = true; cbLen -= 30; } if (hdr.has_eresult && hdr.eresult != (int)EResult::OK) debugLogA("HDR: error code %d", hdr.eresult); // persistent callbacks switch (msgType) { case EMsg::ServiceMethod: case EMsg::ServiceMethodResponse: ProcessServiceResponse(buf, cbLen, hdr); break; default: // find message descriptor first, if succeeded, try to find a message handler then auto md = g_plugin.messages.find(msgType); if (md == g_plugin.messages.end()) { debugLogA("Received message of type %d", msgType); Netlib_Dump(m_ws->getConn(), buf, cbLen, false, 0); } else if (auto *pMessage = protobuf_c_message_unpack(md->second, 0, cbLen, buf)) { debugLogA("Received known message:\n%s", protobuf_c_text_to_string(*pMessage).c_str()); auto mh = g_plugin.messageHandlers.find(msgType); if (mh != g_plugin.messageHandlers.end()) (this->*(mh->second))(*pMessage, hdr); protobuf_c_message_free_unpacked(pMessage, 0); } } } void CSteamProto::ProcessServiceResponse(const uint8_t *buf, size_t cbLen, const CMsgProtoBufHeader &hdr) { char *tmpName = NEWSTR_ALLOCA(hdr.target_job_name); char *p = strchr(tmpName, '.'); if (!p) { debugLogA("Invalid service function: %s", hdr.target_job_name); return; } *p = 0; auto it = g_plugin.services.find(tmpName); if (it == g_plugin.services.end()) { debugLogA("Unregistered service module: %s", tmpName); return; } *p = '.'; auto pHandler = g_plugin.serviceHandlers.find(tmpName); if (pHandler == g_plugin.serviceHandlers.end()) { debugLogA("Unsupported service function: %s", hdr.target_job_name); return; } if (char *p1 = strchr(++p, '#')) *p1 = 0; if (auto *pMethod = protobuf_c_service_descriptor_get_method_by_name(it->second, p)) { auto *pDescr = (hdr.jobid_target == -1) ? pMethod->input : pMethod->output; if (auto *pMessage = protobuf_c_message_unpack(pDescr, 0, cbLen, buf)) { debugLogA("Processing service message: %s\n%s", hdr.target_job_name, protobuf_c_text_to_string(*pMessage).c_str()); (this->*(pHandler->second))(*pMessage, hdr); protobuf_c_message_free_unpacked(pMessage, 0); } } else debugLogA("Unregistered service method: %s", hdr.target_job_name); } ///////////////////////////////////////////////////////////////////////////////////////// void CSteamProto::WSSend(EMsg msgType, const ProtobufCppMessage &msg) { CMsgProtoBufHeader hdr; hdr.has_client_sessionid = hdr.has_steamid = hdr.has_jobid_source = hdr.has_jobid_target = true; hdr.steamid = m_iSteamId; hdr.client_sessionid = m_iSessionId; switch (msgType) { case EMsg::ClientHello: hdr.jobid_source = -1; break; default: hdr.jobid_source = getRandomInt(); break; } hdr.jobid_target = -1; WSSendHeader(msgType, hdr, msg); } void CSteamProto::WSSendRaw(EMsg msgType, const MBinBuffer &body) { MBinBuffer payload; payload << (uint32_t)(int)msgType << uint8_t(36) << uint16_t(2) << uint64_t(-1) << getRandomInt() << uint8_t(239) << m_iSteamId << m_iSessionId; payload.append(body); if (m_ws) m_ws->sendBinary(payload.data(), payload.length()); } void CSteamProto::WSSendHeader(EMsg msgType, const CMsgProtoBufHeader &hdr, const ProtobufCppMessage &msg) { debugLogA("Message sent:\n%s", protobuf_c_text_to_string(msg).c_str()); uint32_t hdrLen = (uint32_t)protobuf_c_message_get_packed_size(&hdr); MBinBuffer hdrbuf(hdrLen); protobuf_c_message_pack(&hdr, hdrbuf.data()); hdrbuf.appendBefore(&hdrLen, sizeof(hdrLen)); uint32_t type = (uint32_t)msgType; type |= STEAM_PROTOCOL_MASK; hdrbuf.appendBefore(&type, sizeof(type)); MBinBuffer body(protobuf_c_message_get_packed_size(&msg)); protobuf_c_message_pack(&msg, body.data()); hdrbuf.append(body); if (m_ws) m_ws->sendBinary(hdrbuf.data(), hdrbuf.length()); } void CSteamProto::WSSendAnon(const char *pszServiceName, const ProtobufCppMessage &msg) { CMsgProtoBufHeader hdr; hdr.has_client_sessionid = hdr.has_steamid = hdr.has_jobid_source = hdr.has_jobid_target = true; hdr.jobid_source = getRandomInt(); hdr.jobid_target = -1; hdr.target_job_name = (char *)pszServiceName; WSSendHeader(EMsg::ServiceMethodCallFromClientNonAuthed, hdr, msg); } void CSteamProto::WSSendService(const char *pszServiceName, const ProtobufCppMessage &msg, void *pInfo) { CMsgProtoBufHeader hdr; hdr.has_client_sessionid = hdr.has_steamid = hdr.has_jobid_source = hdr.has_jobid_target = true; hdr.steamid = m_iSteamId, hdr.client_sessionid = m_iSessionId; hdr.jobid_source = getRandomInt(); hdr.jobid_target = -1; hdr.target_job_name = (char *)pszServiceName; if (pInfo) SetRequestInfo(hdr.jobid_source, pInfo); WSSendHeader(EMsg::ServiceMethodCallFromClient, hdr, msg); } ///////////////////////////////////////////////////////////////////////////////////////// void CSteamProto::SetRequestInfo(uint64_t requestId, void *pInfo) { mir_cslock lck(m_csRequestLock); m_requestInfo[requestId] = pInfo; } void* CSteamProto::GetRequestInfo(uint64_t requestId) { mir_cslock lck(m_csRequestLock); auto it = m_requestInfo.find(requestId); if (it == m_requestInfo.end()) return nullptr; void *pRet = it->second; m_requestInfo.erase(it); return pRet; }