From 1dcce1cbc73844d9f82e1eabeadaa8d20b942ae6 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 28 Jun 2026 08:50:17 +0300 Subject: [PATCH] Push events for webapi and some other functions fixes (#4636) * push events for webapi and some other functions fixes * clang is not my friend --- src/core/libraries/np/np_handler.cpp | 28 ++ src/core/libraries/np/np_handler.h | 2 +- .../libraries/np/np_web_api/np_web_api.cpp | 102 +++- src/core/libraries/np/np_web_api/np_web_api.h | 22 + .../np/np_web_api/np_web_api_internal.cpp | 440 +++++++++++++++++- .../np/np_web_api/np_web_api_internal.h | 24 +- src/shadnet/client.cpp | 30 +- src/shadnet/client.h | 10 + 8 files changed, 633 insertions(+), 25 deletions(-) diff --git a/src/core/libraries/np/np_handler.cpp b/src/core/libraries/np/np_handler.cpp index 28d05fc75..4642f3322 100644 --- a/src/core/libraries/np/np_handler.cpp +++ b/src/core/libraries/np/np_handler.cpp @@ -4,12 +4,14 @@ #include #include +#include #include "common/elf_info.h" #include "common/logging/log.h" #include "core/emulator_settings.h" #include "core/libraries/np/np_error.h" #include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_score/np_score.h" +#include "core/libraries/np/np_web_api/np_web_api.h" #include "core/user_settings.h" #include "imgui/shadnet_notifications_layer.h" #include "np_handler.h" @@ -160,6 +162,9 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons client->onFriendStatus = [this, user_id](const ShadNet::NotifyFriendStatus& n) { OnFriendStatus(user_id, n); }; + client->onWebApiPushEvent = [this, user_id](const ShadNet::NotifyWebApiPushEvent& n) { + OnWebApiPushEvent(user_id, n); + }; client->onAsyncReply = [this, user_id](ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType err, const std::vector& body) { OnScoreReply(user_id, cmd, pkt_id, err, body); @@ -446,6 +451,29 @@ void NpHandler::OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n } } +void NpHandler::OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n) { + LOG_INFO(NpHandler, "user_id={} WebApiPushEvent svc='{}' type='{}' bytes={}", user_id, + n.npServiceName, n.dataType, n.data.size()); + // Forward verbatim to the libSceNpWebApi push-event dispatch.it queues the event + // and delivers it on the game's thread during sceNpCheckCallback to any registered + // (and filter-matching) push-event callback. + NpWebApi::PushEventInput ev; + ev.targetUserId = user_id; + ev.npServiceName = n.npServiceName; + ev.npServiceLabel = n.npServiceLabel; + ev.dataType = n.dataType; + ev.data = n.data; + if (!n.fromNpid.empty()) { + ev.hasFrom = true; + std::strncpy(ev.fromOnlineId.data, n.fromNpid.c_str(), sizeof(ev.fromOnlineId.data) - 1); + } + if (!n.toNpid.empty()) { + ev.hasTo = true; + std::strncpy(ev.toOnlineId.data, n.toNpid.c_str(), sizeof(ev.toOnlineId.data) - 1); + } + NpWebApi::EnqueuePushEvent(ev); +} + void NpHandler::OnLoginResult(s32 user_id, const ShadNet::LoginResult& res) { if (res.error != ShadNet::ErrorType::NoError) { return; diff --git a/src/core/libraries/np/np_handler.h b/src/core/libraries/np/np_handler.h index 7d725d45e..8e47625d8 100644 --- a/src/core/libraries/np/np_handler.h +++ b/src/core/libraries/np/np_handler.h @@ -21,7 +21,6 @@ #include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_score/np_score.h" #include "core/libraries/np/np_score/np_score_ctx.h" -#include "core/libraries/np/np_types.h" #include "core/libraries/rtc/rtc.h" #include "core/libraries/system/userservice.h" #include "shadnet/client.h" @@ -213,6 +212,7 @@ private: void OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n); void OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n); void OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n); + void OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n); void OnLoginResult(s32 user_id, const ShadNet::LoginResult& res); // Async reply dispatch for score commands. Called from the per-user diff --git a/src/core/libraries/np/np_web_api/np_web_api.cpp b/src/core/libraries/np/np_web_api/np_web_api.cpp index 13665c203..295add0e8 100644 --- a/src/core/libraries/np/np_web_api/np_web_api.cpp +++ b/src/core/libraries/np/np_web_api/np_web_api.cpp @@ -6,6 +6,7 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_web_api/np_web_api.h" #include "core/libraries/np/np_web_api/np_web_api_internal.h" @@ -16,6 +17,38 @@ namespace Libraries::Np::NpWebApi { static bool g_is_initialized = false; static s32 g_active_library_contexts = 0; +static std::string DecodeBase64(std::string_view in) { + auto val = [](char c) -> int { + if (c >= 'A' && c <= 'Z') + return c - 'A'; + if (c >= 'a' && c <= 'z') + return c - 'a' + 26; + if (c >= '0' && c <= '9') + return c - '0' + 52; + if (c == '+') + return 62; + if (c == '/') + return 63; + return -1; + }; + std::string out; + int buf = 0, bits = 0; + for (char c : in) { + if (c == '=' || c == '\0') + break; + const int v = val(c); + if (v < 0) + continue; // skip whitespace / invalid chars + buf = (buf << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push_back(static_cast((buf >> bits) & 0xFF)); + } + } + return out; +} + s32 PS4_SYSV_ABI sceNpWebApiCreateContext(s32 libCtxId, OrbisNpOnlineId* onlineId) { if (libCtxId >= 0x8000) { return ORBIS_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID; @@ -137,11 +170,11 @@ s32 PS4_SYSV_ABI sceNpWebApiAbortRequest(s64 requestId) { s32 PS4_SYSV_ABI sceNpWebApiAddHttpRequestHeader(s64 requestId, const char* pFieldName, const char* pValue) { - LOG_ERROR(Lib_NpWebApi, - "called (STUBBED) : requestId = {:#x}, " - "pFieldName = '{}', pValue = '{}'", - requestId, (pFieldName ? pFieldName : "null"), (pValue ? pValue : "null")); - return ORBIS_OK; + LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pFieldName = '{}', pValue = '{}'", + requestId, (pFieldName ? pFieldName : "null"), (pValue ? pValue : "null")); + if (pFieldName == nullptr || pValue == nullptr) + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + return addHttpRequestHeaderInternal(requestId, pFieldName, pValue); } s32 PS4_SYSV_ABI sceNpWebApiAddMultipartPart(s64 requestId, @@ -292,26 +325,27 @@ s32 PS4_SYSV_ABI sceNpWebApiGetConnectionStats(s32 userCtxId, const char* pApiGr } s32 PS4_SYSV_ABI sceNpWebApiGetErrorCode() { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); - return ORBIS_OK; + const s32 code = getLastWebApiError(); + LOG_INFO(Lib_NpWebApi, "called : lastErrorCode = {:#x}", code); + return code; } s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValue(s64 requestId, const char* pFieldName, char* pValue, u64 valueSize) { - LOG_ERROR(Lib_NpWebApi, - "called (STUBBED) : requestId = {:#x}, " - "pFieldName = '{}', pValue = {}, valueSize = {}", - requestId, (pFieldName ? pFieldName : "null"), fmt::ptr(pValue), valueSize); - return ORBIS_OK; + LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pFieldName = '{}', valueSize = {}", + requestId, (pFieldName ? pFieldName : "null"), valueSize); + if (pFieldName == nullptr || pValue == nullptr || valueSize == 0) + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + return getHttpResponseHeaderValueInternal(requestId, pFieldName, pValue, valueSize, nullptr); } s32 PS4_SYSV_ABI sceNpWebApiGetHttpResponseHeaderValueLength(s64 requestId, const char* pFieldName, u64* pValueLength) { - LOG_ERROR(Lib_NpWebApi, - "called (STUBBED) : requestId = {:#x}, " - "pFieldName = '{}', pValueLength = {}", - requestId, (pFieldName ? pFieldName : "null"), fmt::ptr(pValueLength)); - return ORBIS_OK; + LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pFieldName = '{}'", requestId, + (pFieldName ? pFieldName : "null")); + if (pFieldName == nullptr || pValueLength == nullptr) + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + return getHttpResponseHeaderValueInternal(requestId, pFieldName, nullptr, 0, pValueLength); } s32 PS4_SYSV_ABI sceNpWebApiGetHttpStatusCode(s64 requestId, s32* out_status_code) { @@ -570,7 +604,35 @@ s32 PS4_SYSV_ABI sceNpWebApiUnregisterExtdPushEventCallback(s32 titleUserCtxId, s32 PS4_SYSV_ABI sceNpWebApiUtilityParseNpId(const char* pJsonNpId, Libraries::Np::OrbisNpId* pNpId) { - LOG_ERROR(Lib_NpWebApi, "(STUBBED) called"); + if (pJsonNpId == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + // Decode and parse the serialized npId. Format: "@[/][.]". + // The part before '@' is the online-id handle (<=16 bytes); the segments after, + // with their '/' and '.' separators removed, concatenate into the 8-byte opt + const std::string decoded = DecodeBase64(pJsonNpId); + std::string handle; + std::string opt; + const size_t at = decoded.find('@'); + if (at == std::string::npos) { + handle = decoded; + } else { + handle = decoded.substr(0, at); + for (size_t i = at + 1; i < decoded.size(); ++i) { + const char c = decoded[i]; + if (c != '/' && c != '.') { + opt.push_back(c); + } + } + } + if (pNpId != nullptr) { + std::memset(pNpId, 0, sizeof(Libraries::Np::OrbisNpId)); + std::memcpy(pNpId->handle.data, handle.data(), + std::min(handle.size(), ORBIS_NP_ONLINEID_MAX_LENGTH)); + std::memcpy(pNpId->opt, opt.data(), std::min(opt.size(), sizeof(pNpId->opt))); + pNpId->reserved[0] = 0x01; // valid-handle flag, as the PRX sets + } + LOG_INFO(Lib_NpWebApi, "parsed npId -> handle='{}' opt='{}'", handle, opt); return ORBIS_OK; } @@ -813,6 +875,10 @@ s32 PS4_SYSV_ABI Func_F9A32E8685627436() { } void RegisterLib(Core::Loader::SymbolsResolver* sym) { + // Drain queued push events on the game's thread during sceNpCheckCallback, so + // registered push-event callbacks fire on a guest thread (see DrainPushEvents). + Libraries::Np::NpManager::RegisterNpCallback("npwebapi_push", DrainPushEvents); + LIB_FUNCTION("x1Y7yiYSk7c", "libSceNpWebApiCompat", 1, "libSceNpWebApi", sceNpWebApiCreateContext); LIB_FUNCTION("y5Ta5JCzQHY", "libSceNpWebApiCompat", 1, "libSceNpWebApi", diff --git a/src/core/libraries/np/np_web_api/np_web_api.h b/src/core/libraries/np/np_web_api/np_web_api.h index 8dd9441e0..2598ec8b1 100644 --- a/src/core/libraries/np/np_web_api/np_web_api.h +++ b/src/core/libraries/np/np_web_api/np_web_api.h @@ -3,6 +3,9 @@ #pragma once +#include +#include +#include #include "common/types.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_types.h" @@ -124,5 +127,24 @@ using OrbisNpWebApiInternalServicePushEventCallbackA = PS4_SYSV_ABI void (*)(); using OrbisNpWebApiNotificationCallback = PS4_SYSV_ABI void (*)(); // dummy +// --- Push-event delivery (emulator-internal) --------------------------------- +// A producer (NpHandler / shadNet notification ingress) calls EnqueuePushEvent from +// any thread. The event is queued and dispatched to registered push-event callbacks +// on the game's thread during sceNpCheckCallback.This mirrors the real lib, where push events +// arrive via the NP push/notification subsystem and are pumped on the callback-check thread. +struct PushEventInput { + Libraries::UserService::OrbisUserServiceUserId targetUserId = 0; + std::string npServiceName; // e.g. "np:service:..."; empty for basic push + OrbisNpServiceLabel npServiceLabel = 0; + std::string dataType; // OrbisNpWebApiPushEventDataType value + std::string data; // payload bytes + OrbisNpOnlineId fromOnlineId{}; + bool hasFrom = false; + OrbisNpOnlineId toOnlineId{}; + bool hasTo = false; + std::vector> extdData; // (key,value) for extd push +}; +void EnqueuePushEvent(const PushEventInput& ev); + void RegisterLib(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Np::NpWebApi \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api/np_web_api_internal.cpp b/src/core/libraries/np/np_web_api/np_web_api_internal.cpp index f5d49c166..40a488a83 100644 --- a/src/core/libraries/np/np_web_api/np_web_api_internal.cpp +++ b/src/core/libraries/np/np_web_api/np_web_api_internal.cpp @@ -1,6 +1,13 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include +#include +#include #include #include "common/elf_info.h" #include "core/emulator_settings.h" @@ -16,6 +23,11 @@ namespace Libraries::Np::NpWebApi { static std::recursive_mutex g_global_mutex; static std::map g_contexts; static s32 g_library_context_count = 0; + +// Last WebApi error code parsed from an error response body +static thread_local s32 g_last_webapi_error = ORBIS_OK; + +static s32 captureWebApiError(const char* body, u64 len); static s32 g_user_context_count = 0; static s32 g_handle_count = 0; static s32 g_push_event_filter_count = 0; @@ -568,7 +580,7 @@ void checkRequestTimeout(OrbisNpWebApiRequest* request) { } s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s8 flag, - const OrbisNpWebApiResponseInformationOption* pRespInfoOption) { + OrbisNpWebApiResponseInformationOption* pRespInfoOption) { OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); if (context == nullptr) { return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; @@ -710,6 +722,12 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s "be unauthenticated (expect 401 from server)", user_context->userId, request->userPath); } + + // Replay app-supplied headers + for (const auto& [hname, hvalue] : request->userHeaders) { + Libraries::Http::sceHttpAddRequestHeader(req_id, hname.c_str(), hvalue.c_str(), + /*mode=*/0); + } } setRequestState(request, 4); @@ -729,10 +747,52 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s requestId, request->userApiGroup, request->userPath, magic_enum::enum_name(request->userMethod), request->http_request_id); + s32 sendResult = ORBIS_OK; + if (flag != 0) { + s32 status = 0; + if (Libraries::Http::sceHttpGetStatusCode(request->http_request_id, &status) >= 0) { + if (pRespInfoOption != nullptr) { + pRespInfoOption->httpStatus = status; + } + if (status >= 400) { + std::string errBody; + char buf[256]; + for (;;) { + const s32 n = Libraries::Http::sceHttpReadData(request->http_request_id, buf, + sizeof(buf)); + if (n <= 0) + break; + errBody.append(buf, static_cast(n)); + if (errBody.size() > 64u * 1024u) + break; // sanity cap on a runaway error body + } + if (pRespInfoOption != nullptr) { + pRespInfoOption->responseDataSize = errBody.size(); + char* const dst = pRespInfoOption->pErrorObject; + const u64 cap = pRespInfoOption->errorObjectSize; + if (dst != nullptr && cap > 0) { + const u64 n = std::min(errBody.size(), cap - 1); + if (n > 0) + std::memcpy(dst, errBody.data(), n); + dst[n] = '\0'; + } + } + const s32 npErr = captureWebApiError(errBody.data(), errBody.size()); + if (npErr > 0 && npErr < 0xc00000) { + sendResult = static_cast(0x82000000u | static_cast(npErr)); + } else if (status >= 100 && status < 600) { + sendResult = static_cast(0x82f00000u | static_cast(status)); + } else { + sendResult = static_cast(0x82ffffffu); + } + } + } + } + releaseRequest(request); releaseUserContext(user_context); releaseContext(context); - return ORBIS_OK; + return sendResult; } s32 abortRequestInternal(OrbisNpWebApiContext* context, OrbisNpWebApiUserContext* userContext, @@ -1022,10 +1082,13 @@ s32 createPushEventFilterInternal(OrbisNpWebApiContext* context, filter->parentContext = context; filter->filterId = filterId; + LOG_INFO(Lib_NpWebApi, "createPushEventFilter: filterId={} dataTypeParams={}", filterId, + filterParamNum); if (pFilterParam != nullptr && filterParamNum != 0) { for (u64 param_idx = 0; param_idx < filterParamNum; param_idx++) { OrbisNpWebApiPushEventFilterParameter copy = OrbisNpWebApiPushEventFilterParameter{}; memcpy(©, &pFilterParam[param_idx], sizeof(OrbisNpWebApiPushEventFilterParameter)); + LOG_INFO(Lib_NpWebApi, " filterParam[{}] dataType='{}'", param_idx, copy.dataType.val); filter->filterParams.emplace_back(copy); } } @@ -1353,12 +1416,23 @@ s32 createExtendedPushEventFilterInternal( filter->npServiceLabel = npServiceLabel; + LOG_INFO(Lib_NpWebApi, + "createExtdPushEventFilter: filterId={} service='{}' label={:#x} dataTypeParams={}", + filterId, pNpServiceName ? pNpServiceName : "null", npServiceLabel, filterParamNum); if (pFilterParam != nullptr && filterParamNum != 0) { for (u64 param_idx = 0; param_idx < filterParamNum; param_idx++) { OrbisNpWebApiExtdPushEventFilterParameter copy = OrbisNpWebApiExtdPushEventFilterParameter{}; memcpy(©, &pFilterParam[param_idx], sizeof(OrbisNpWebApiExtdPushEventFilterParameter)); + LOG_INFO(Lib_NpWebApi, " filterParam[{}] dataType='{}' extdKeys={}", param_idx, + copy.dataType.val, copy.extdDataKeyNum); // debug + if (copy.pExtdDataKey != nullptr) { + for (u64 k = 0; k < copy.extdDataKeyNum; k++) { + LOG_INFO(Lib_NpWebApi, " extdDataKey[{}]='{}'", k, + copy.pExtdDataKey[k].val); // debug + } + } filter->filterParams.emplace_back(copy); // TODO: Every parameter is registered with an extended data filter through // sceNpPushRegisterExtendedDataFilter @@ -1499,6 +1573,156 @@ s32 PS4_SYSV_ABI getHttpRequestIdFromRequest(OrbisNpWebApiRequest* request) return request->http_request_id; } +// Request/response header support + +bool iEquals(const char* a, u64 aLen, const char* b) { + u64 i = 0; + for (; i < aLen && b[i]; ++i) { + if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i])) + return false; + } + return i == aLen && b[i] == '\0'; +} + +// Locate `field`'s value in a raw HTTP response header block (CRLF-separated +// "Name: Value" lines, possibly led by a status line). Returns false if absent. +bool findHeaderValue(const char* block, u64 blockLen, const char* field, std::string& outValue) { + u64 i = 0; + while (i < blockLen) { + u64 lineStart = i; + while (i < blockLen && block[i] != '\n') + ++i; + u64 lineEnd = i; // points at '\n' or end + if (i < blockLen) + ++i; // step past '\n' + // Trim a trailing '\r'. + if (lineEnd > lineStart && block[lineEnd - 1] == '\r') + --lineEnd; + // Split on the first ':'. + u64 colon = lineStart; + while (colon < lineEnd && block[colon] != ':') + ++colon; + if (colon >= lineEnd) + continue; // no colon (status line / blank) + u64 nameLen = colon - lineStart; + if (!iEquals(block + lineStart, nameLen, field)) + continue; + u64 vs = colon + 1; + while (vs < lineEnd && (block[vs] == ' ' || block[vs] == '\t')) + ++vs; + outValue.assign(block + vs, lineEnd - vs); + return true; + } + return false; +} + +// extraction of the numeric WebApi error code from an error JSON body +// (e.g. {"error":{"code":2240512,...}} or {"code":...}). Returns true on success. +bool parseErrorCode(const char* body, u64 len, s32& out) { + std::string_view sv(body, len); + size_t k = sv.find("\"code\""); + if (k == std::string_view::npos) + return false; + k += 6; + while (k < sv.size() && (sv[k] == ' ' || sv[k] == ':' || sv[k] == '"')) + ++k; + bool neg = false; + if (k < sv.size() && (sv[k] == '-' || sv[k] == '+')) { + neg = sv[k] == '-'; + ++k; + } + if (k >= sv.size() || !std::isdigit((unsigned char)sv[k])) + return false; + long long v = 0; + while (k < sv.size() && std::isdigit((unsigned char)sv[k])) { + v = v * 10 + (sv[k] - '0'); + ++k; + } + out = static_cast(neg ? -v : v); + return true; +} + +static s32 captureWebApiError(const char* body, u64 len) { + s32 code = 0; + if (body != nullptr && len > 0 && parseErrorCode(body, len, code)) { + g_last_webapi_error = code; + return code; + } + return 0; +} + +s32 getLastWebApiError() { + return g_last_webapi_error; +} + +s32 addHttpRequestHeaderInternal(s64 requestId, const char* pFieldName, const char* pValue) { + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + OrbisNpWebApiRequest* request = findRequest(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + request->userHeaders.emplace_back(pFieldName, pValue); + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +s32 getHttpResponseHeaderValueInternal(s64 requestId, const char* pFieldName, char* pValue, + u64 valueSize, u64* pValueLength) { + OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); + if (context == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + } + OrbisNpWebApiUserContext* user_context = findUserContext(context, requestId >> 0x20); + if (user_context == nullptr) { + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_USER_CONTEXT_NOT_FOUND; + } + OrbisNpWebApiRequest* request = findRequestAndMarkBusy(user_context, requestId); + if (request == nullptr) { + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_REQUEST_NOT_FOUND; + } + + char* block = nullptr; + u64 blockSize = 0; + const s32 httpReqId = getHttpRequestIdFromRequest(request); + const s32 err = Libraries::Http::sceHttpGetAllResponseHeaders(httpReqId, &block, &blockSize); + + s32 result = ORBIS_OK; + if (err < 0) { + result = err; + } else { + std::string value; + const bool found = block != nullptr && findHeaderValue(block, blockSize, pFieldName, value); + if (pValueLength != nullptr) + *pValueLength = found ? value.size() : 0; + if (pValue != nullptr && valueSize > 0) { + const u64 n = found ? std::min(value.size(), valueSize - 1) : 0; + if (n > 0) + std::memcpy(pValue, value.data(), n); + pValue[n] = '\0'; + } + } + + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return result; +} + s32 PS4_SYSV_ABI getHttpStatusCodeInternal(s64 requestId, s32* out_status_code) { s32 status_code; OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); @@ -1640,6 +1864,22 @@ s32 PS4_SYSV_ABI readDataInternal(s64 requestId, void* pData, u64 size) { result = ORBIS_NP_WEBAPI_ERROR_ABORTED; } else { result = offset; + // Surface the server error code for sceNpWebApiGetErrorCode() when the body + // just read belongs to an error response + if (offset > 0) { + s32 sc = 0; + if (Libraries::Http::sceHttpGetStatusCode(getHttpRequestIdFromRequest(request), &sc) >= + 0 && + sc >= 400) { + captureWebApiError(reinterpret_cast(pData), offset); + } + } + } + + if (result > 0) { + LOG_INFO(Lib_NpWebApi, "readData reqId={:#x} -> {} bytes: {:.256s}", requestId, result, + std::string(reinterpret_cast(pData), + std::min(result, 256))); // debug to be removed } unlockContext(context); @@ -1653,4 +1893,200 @@ s32 PS4_SYSV_ABI readDataInternal(s64 requestId, void* pData, u64 size) { return result; } +// --- Push-event delivery ----------------------------------------------------- +// +// Mirrors the native lib: notificationCallbackFunc -> FUN_0100e220 (extended) and the +// service dispatch, with matching done by FUN_010049c0. Natively this is driven by the +// NP manager's push listener thread (mnp:usr:npweblis) + +using ServiceCb = PS4_SYSV_ABI void (*)(s32, s32, const char*, OrbisNpServiceLabel, + const OrbisNpWebApiPushEventDataType*, const char*, u64, + void*); +struct PushPeerAddress { + OrbisNpOnlineId onlineId; + s32 platform; +}; +using BasicCb = PS4_SYSV_ABI void (*)(s32, s32, const PushPeerAddress*, const PushPeerAddress*, + const OrbisNpWebApiPushEventDataType*, const char*, u64, + void*); +using ExtdCbA = PS4_SYSV_ABI void (*)(s32, s32, const char*, OrbisNpServiceLabel, + const OrbisNpPeerAddressA*, const OrbisNpOnlineId*, + const OrbisNpPeerAddressA*, const OrbisNpOnlineId*, + const OrbisNpWebApiPushEventDataType*, const char*, u64, + const OrbisNpWebApiExtdPushEventExtdData*, u64, void*); + +std::mutex g_push_mutex; +std::deque g_push_queue; + +template +bool filterMatches(const Filter* flt, const std::string& evServiceName, bool evHasServiceName, + const std::string& evDataType) { + const bool catch_all = flt->npServiceName.empty() && flt->npServiceLabel == 0xffffffffu; + if (catch_all) { + if (evHasServiceName) { + return false; + } + } else if (!evHasServiceName || flt->npServiceName != evServiceName) { + return false; + } + for (const auto& p : flt->filterParams) { + if (evDataType == p.dataType.val) { + return true; + } + } + return false; +} + +// Network-thread hand-off only,the event is dispatched later from DrainPushEvents. +void EnqueuePushEvent(const PushEventInput& ev) { + std::scoped_lock lk{g_push_mutex}; + g_push_queue.push_back(ev); +} + +// Runs on the game thread from sceNpCheckCallback +void DrainPushEvents() { + std::deque local; + { + std::scoped_lock lk{g_push_mutex}; + if (g_push_queue.empty()) { + return; + } + local.swap(g_push_queue); + } + + for (const PushEventInput& ev : local) { + const bool ev_has_service = !ev.npServiceName.empty(); + OrbisNpWebApiPushEventDataType dt{}; + std::snprintf(dt.val, sizeof(dt.val), "%s", ev.dataType.c_str()); + const OrbisNpOnlineId* from_p = ev.hasFrom ? &ev.fromOnlineId : nullptr; + const OrbisNpOnlineId* to_p = ev.hasTo ? &ev.toOnlineId : nullptr; + + std::scoped_lock gl{g_global_mutex}; + for (auto& [libId, context] : g_contexts) { + if (context == nullptr) { + continue; + } + std::scoped_lock cl{context->contextLock}; + for (auto& [ucKey, uc] : context->userContexts) { + if (uc == nullptr) { + continue; + } + if (uc->userId != ev.targetUserId) { + continue; + } + const s32 title_user_ctx_id = ucKey; + + for (auto& [cbId, cb] : uc->extendedPushEventCallbacks) { + if (cb == nullptr) { + continue; + } + void (*raw)() = cb->cbFuncA ? reinterpret_cast(cb->cbFuncA) + : cb->cbFunc ? reinterpret_cast(cb->cbFunc) + : nullptr; + if (raw == nullptr) { + continue; + } + auto fit = context->extendedPushEventFilters.find(cb->filterId); + if (fit == context->extendedPushEventFilters.end()) { + continue; + } + const OrbisNpWebApiExtendedPushEventFilter* flt = fit->second; + if (!filterMatches(flt, ev.npServiceName, ev_has_service, ev.dataType)) { + continue; + } + std::vector exarr; + exarr.reserve(ev.extdData.size()); + for (auto& [k, v] : ev.extdData) { + OrbisNpWebApiExtdPushEventExtdData e{}; + std::snprintf(e.extdDataKey.val, sizeof(e.extdDataKey.val), "%s", + k.c_str()); + e.pData = const_cast(v.data()); + e.dataLen = v.size(); + exarr.push_back(e); + } + // Service name/label come from the FILTER + const char* svc = + flt->npServiceName.empty() ? nullptr : flt->npServiceName.c_str(); + // pData/pExtdData are NULL (and the counts 0) when the event + // carries no payload / no matching extended data. + const char* ext_data = ev.data.empty() ? nullptr : ev.data.data(); + const OrbisNpWebApiExtdPushEventExtdData* ext_arr = + exarr.empty() ? nullptr : exarr.data(); + + LOG_INFO( + Lib_NpWebApi, + "DrainPushEvents: invoking extd cb ctx={:#x} cbId={} dataType='{}'", + title_user_ctx_id, cbId, + ev.dataType); // debug confirm the listener callback fires. to be removed + reinterpret_cast(raw)( + title_user_ctx_id, cbId, svc, flt->npServiceLabel, nullptr, to_p, nullptr, + from_p, &dt, ext_data, ev.data.size(), ext_arr, exarr.size(), cb->pUserArg); + } + + // Service push + for (auto& [cbId, cb] : uc->servicePushEventCallbacks) { + if (cb == nullptr || cb->cbFunc == nullptr) { + continue; + } + auto fit = context->servicePushEventFilters.find(cb->filterId); + if (fit == context->servicePushEventFilters.end()) { + continue; + } + const OrbisNpWebApiServicePushEventFilter* flt = fit->second; + if (!filterMatches(flt, ev.npServiceName, ev_has_service, ev.dataType)) { + continue; + } + const char* svc = + flt->npServiceName.empty() ? nullptr : flt->npServiceName.c_str(); + const char* svc_data = ev.data.empty() ? nullptr : ev.data.data(); + reinterpret_cast(reinterpret_cast(cb->cbFunc))( + title_user_ctx_id, cbId, svc, flt->npServiceLabel, &dt, svc_data, + ev.data.size(), cb->pUserArg); + } + + // Basic push + if (!ev_has_service) { + for (auto& [cbId, cb] : uc->pushEventCallbacks) { + if (cb == nullptr || cb->cbFunc == nullptr) { + continue; + } + auto fit = context->pushEventFilters.find(cb->filterId); + if (fit == context->pushEventFilters.end()) { + continue; + } + const OrbisNpWebApiPushEventFilter* flt = fit->second; + bool matched = false; + for (const auto& p : flt->filterParams) { + if (ev.dataType == p.dataType.val) { + matched = true; + break; + } + } + if (!matched) { + continue; + } + LOG_INFO(Lib_NpWebApi, + "DrainPushEvents: invoking basic cb ctx={:#x} cbId={} " + "dataType='{}'", + title_user_ctx_id, cbId, + ev.dataType); // debug confirm the listener callback fires. to be + // removed + PushPeerAddress to_peer{}; // notified user (self) + PushPeerAddress from_peer{}; // user that caused the event + if (ev.hasTo) { + to_peer.onlineId = ev.toOnlineId; + } + if (ev.hasFrom) { + from_peer.onlineId = ev.fromOnlineId; + } + const char* p_data = ev.data.empty() ? nullptr : ev.data.data(); + reinterpret_cast(reinterpret_cast(cb->cbFunc))( + title_user_ctx_id, cbId, &to_peer, &from_peer, &dt, p_data, + ev.data.size(), cb->pUserArg); + } + } + } + } + } +} }; // namespace Libraries::Np::NpWebApi diff --git a/src/core/libraries/np/np_web_api/np_web_api_internal.h b/src/core/libraries/np/np_web_api/np_web_api_internal.h index 44d960d30..7ccf6ef28 100644 --- a/src/core/libraries/np/np_web_api/np_web_api_internal.h +++ b/src/core/libraries/np/np_web_api/np_web_api_internal.h @@ -4,6 +4,9 @@ #pragma once #include #include +#include +#include +#include #include "common/logging/log.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" @@ -63,6 +66,9 @@ struct OrbisNpWebApiRequest { OrbisNpWebApiHttpMethod userMethod; u64 userContentLength; std::string userContentType; + // App-supplied request headers (sceNpWebApiAddHttpRequestHeader), replayed onto + // the libSceHttp request in the send path after the lib-managed CT/Authorization. + std::vector> userHeaders; bool multipart; bool aborted; bool sent; @@ -195,15 +201,27 @@ bool isRequestBusy(OrbisNpWebApiRequest* request); // FUN_0100c1b0 s32 setRequestTimeout(s64 requestId, u32 timeout); // FUN_01003610 void startRequestTimer(OrbisNpWebApiRequest* request); // FUN_0100c0d0 void checkRequestTimeout(OrbisNpWebApiRequest* request); // FUN_0100c130 -s32 sendRequest( - s64 requestId, s32 partIndex, const void* data, u64 dataSize, s8 flag, - const OrbisNpWebApiResponseInformationOption* pResponseInformationOption); // FUN_01001c50 +s32 sendRequest(s64 requestId, s32 partIndex, const void* data, u64 dataSize, s8 flag, + OrbisNpWebApiResponseInformationOption* pResponseInformationOption); // FUN_01001c50 s32 abortRequestInternal(OrbisNpWebApiContext* context, OrbisNpWebApiUserContext* userContext, OrbisNpWebApiRequest* request); // FUN_01001b70 s32 abortRequest(s64 requestId); // FUN_01002c70 void releaseRequest(OrbisNpWebApiRequest* request); // FUN_01009fb0 s32 deleteRequest(s64 requestId); // FUN_010019a0 +// Request/response header helpers +s32 addHttpRequestHeaderInternal(s64 requestId, const char* pFieldName, const char* pValue); +s32 getHttpResponseHeaderValueInternal(s64 requestId, const char* pFieldName, char* pValue, + u64 valueSize, u64* pValueLength); +// Returns the WebApi error code captured from the most recent error response body +// read on this thread (sceNpWebApiGetErrorCode). 0 if none. +s32 getLastWebApiError(); + +// Game-thread pump: drains queued push events (EnqueuePushEvent) and dispatches them +// to matching registered callbacks. Registered into the NpManager callback pump in +// RegisterLib so it runs during sceNpCheckCallback. +void DrainPushEvents(); + // Handle functions s32 createHandleInternal(OrbisNpWebApiContext* context); // FUN_01007730 s32 createHandle(s32 libCtxId); // FUN_01002ee0 diff --git a/src/shadnet/client.cpp b/src/shadnet/client.cpp index f6c62a975..3d996d830 100644 --- a/src/shadnet/client.cpp +++ b/src/shadnet/client.cpp @@ -653,7 +653,10 @@ void ShadNetClient::HandleGetTokenReply(const std::vector& payload) { void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector& payload) { // Notification payload = u32 LE blob size + proto bytes const std::string blob = ExtractBlob(payload, 0); - if (blob.empty()) { + if (blob.empty() && + static_cast(cmd_raw) != NotificationType::WebApiPushEvent) { + // WebApiPushEvent is multi-field,its first field (service name) may be empty + // legitimately, so it parses its own payload below rather than relying on blob. LOG_WARNING(ShadNet, "Empty notification payload type={}", cmd_raw); return; } @@ -714,6 +717,31 @@ void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector& paylo onFriendStatus(n); break; } + case NotificationType::WebApiPushEvent: { + // Length-prefixed fields: npServiceName, npServiceLabel(u32 LE), dataType, + // data, fromNpid, toNpid. ExtractBlob returns {} past the end, so a short + // payload degrades to empty trailing fields rather than reading OOB. + NotifyWebApiPushEvent n; + int off = 0; + n.npServiceName = ExtractBlob(payload, off); + off += 4 + static_cast(n.npServiceName.size()); + if (off + 4 <= static_cast(payload.size())) { + n.npServiceLabel = GetLE32(payload.data() + off); + off += 4; + } + n.dataType = ExtractBlob(payload, off); + off += 4 + static_cast(n.dataType.size()); + n.data = ExtractBlob(payload, off); + off += 4 + static_cast(n.data.size()); + n.fromNpid = ExtractBlob(payload, off); + off += 4 + static_cast(n.fromNpid.size()); + n.toNpid = ExtractBlob(payload, off); + LOG_INFO(ShadNet, "WebApiPushEvent svc='{}' type='{}' from='{}' bytes={}", n.npServiceName, + n.dataType, n.fromNpid, n.data.size()); + if (onWebApiPushEvent) + onWebApiPushEvent(n); + break; + } default: LOG_DEBUG(ShadNet, "Unknown notification type {}", cmd_raw); break; diff --git a/src/shadnet/client.h b/src/shadnet/client.h index 747fff56d..4aa8ee1f1 100644 --- a/src/shadnet/client.h +++ b/src/shadnet/client.h @@ -93,6 +93,7 @@ enum class NotificationType : u16 { FriendNew = 6, FriendLost = 7, FriendStatus = 8, + WebApiPushEvent = 17, // Generic NP WebApi push event }; enum class ErrorType : uint8_t { @@ -179,6 +180,14 @@ struct NotifyFriendStatus { bool online = false; u64 timestamp = 0; }; +struct NotifyWebApiPushEvent { + std::string npServiceName; // may be empty (catch-all listeners match any) + u32 npServiceLabel = 0; + std::string dataType; // e.g. "np:service:..." + std::string data; // raw event body (typically PSN-format JSON) + std::string fromNpid; // may be empty + std::string toNpid; // may be empty +}; // ShadNetClient @@ -214,6 +223,7 @@ public: std::function onFriendNew; std::function onFriendLost; std::function onFriendStatus; + std::function onWebApiPushEvent; // Async reply callback. // cmd —command this reply is for (matches the request's cmd) // pkt_id —packet id echoed back from the original request header