Push events for webapi and some other functions fixes (#4636)

* push events for webapi and some other functions fixes

* clang is not my friend
This commit is contained in:
georgemoralis 2026-06-28 08:50:17 +03:00 committed by GitHub
parent 9c0310909e
commit 1dcce1cbc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 633 additions and 25 deletions

View File

@ -4,12 +4,14 @@
#include <algorithm>
#include <chrono>
#include <cstring>
#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<u8>& 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;

View File

@ -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

View File

@ -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<char>((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: "<handle>@<seg>[/<seg>][.<seg>]".
// 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<size_t>(handle.size(), ORBIS_NP_ONLINEID_MAX_LENGTH));
std::memcpy(pNpId->opt, opt.data(), std::min<size_t>(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",

View File

@ -3,6 +3,9 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
#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<std::pair<std::string, std::string>> extdData; // (key,value) for extd push
};
void EnqueuePushEvent(const PushEventInput& ev);
void RegisterLib(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::Np::NpWebApi

View File

@ -1,6 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <deque>
#include <string>
#include <string_view>
#include <magic_enum/magic_enum.hpp>
#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<s32, OrbisNpWebApiContext*> 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<size_t>(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<u64>(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<s32>(0x82000000u | static_cast<u32>(npErr));
} else if (status >= 100 && status < 600) {
sendResult = static_cast<s32>(0x82f00000u | static_cast<u32>(status));
} else {
sendResult = static_cast<s32>(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(&copy, &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(&copy, &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<s32>(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<u64>(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<const char*>(pData), offset);
}
}
}
if (result > 0) {
LOG_INFO(Lib_NpWebApi, "readData reqId={:#x} -> {} bytes: {:.256s}", requestId, result,
std::string(reinterpret_cast<const char*>(pData),
std::min<u64>(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<PushEventInput> g_push_queue;
template <typename Filter>
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<PushEventInput> 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<void (*)()>(cb->cbFuncA)
: cb->cbFunc ? reinterpret_cast<void (*)()>(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<OrbisNpWebApiExtdPushEventExtdData> 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<char*>(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<ExtdCbA>(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<ServiceCb>(reinterpret_cast<void (*)()>(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<BasicCb>(reinterpret_cast<void (*)()>(cb->cbFunc))(
title_user_ctx_id, cbId, &to_peer, &from_peer, &dt, p_data,
ev.data.size(), cb->pUserArg);
}
}
}
}
}
}
}; // namespace Libraries::Np::NpWebApi

View File

@ -4,6 +4,9 @@
#pragma once
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
#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<std::pair<std::string, std::string>> 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

View File

@ -653,7 +653,10 @@ void ShadNetClient::HandleGetTokenReply(const std::vector<u8>& payload) {
void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector<u8>& payload) {
// Notification payload = u32 LE blob size + proto bytes
const std::string blob = ExtractBlob(payload, 0);
if (blob.empty()) {
if (blob.empty() &&
static_cast<NotificationType>(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<u8>& 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<int>(n.npServiceName.size());
if (off + 4 <= static_cast<int>(payload.size())) {
n.npServiceLabel = GetLE32(payload.data() + off);
off += 4;
}
n.dataType = ExtractBlob(payload, off);
off += 4 + static_cast<int>(n.dataType.size());
n.data = ExtractBlob(payload, off);
off += 4 + static_cast<int>(n.data.size());
n.fromNpid = ExtractBlob(payload, off);
off += 4 + static_cast<int>(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;

View File

@ -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<void(const NotifyFriendNew&)> onFriendNew;
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
std::function<void(const NotifyWebApiPushEvent&)> 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