From d460c94b60658deaadb5dcd3400c23abb965cd0a Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 3 Jul 2026 17:09:49 +0300 Subject: [PATCH] Implemented MultipartPart content for npwebapi (#4663) * implemented MultipartPart content * fixed possible duplicate content-type resubmit --- .../libraries/np/np_web_api/np_web_api.cpp | 20 +- .../np/np_web_api/np_web_api_internal.cpp | 178 +++++++++++++++++- .../np/np_web_api/np_web_api_internal.h | 13 ++ 3 files changed, 194 insertions(+), 17 deletions(-) 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 295add0e8..0c89a9fb9 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 @@ -180,15 +180,9 @@ s32 PS4_SYSV_ABI sceNpWebApiAddHttpRequestHeader(s64 requestId, const char* pFie s32 PS4_SYSV_ABI sceNpWebApiAddMultipartPart(s64 requestId, const OrbisNpWebApiMultipartPartParameter* pParam, s32* pIndex) { - LOG_INFO(Lib_NpWebApi, - "called (STUBBED) : requestId = {:#x}, " - "pParam = {}, pIndex = {}", - requestId, fmt::ptr(pParam), fmt::ptr(pIndex)); - if (pParam) { - LOG_ERROR(Lib_NpWebApi, " Part params: headerNum = {}, contentLength = {}", - pParam->headerNum, pParam->contentLength); - } - return ORBIS_OK; + LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, headerNum = {}, contentLength = {}", + requestId, (pParam ? pParam->headerNum : 0), (pParam ? pParam->contentLength : 0)); + return addMultipartPart(requestId, pParam, pIndex); } void PS4_SYSV_ABI sceNpWebApiCheckTimeout() { @@ -570,11 +564,9 @@ s32 PS4_SYSV_ABI sceNpWebApiSetMaxConnection(s32 libCtxId, s32 maxConnection) { s32 PS4_SYSV_ABI sceNpWebApiSetMultipartContentType(s64 requestId, const char* pTypeName, const char* pBoundary) { - LOG_ERROR(Lib_NpWebApi, - "called (STUBBED) : requestId = {:#x}, " - "pTypeName = '{}', pBoundary = '{}'", - requestId, (pTypeName ? pTypeName : "null"), (pBoundary ? pBoundary : "null")); - return ORBIS_OK; + LOG_INFO(Lib_NpWebApi, "called : requestId = {:#x}, pTypeName = '{}', pBoundary = '{}'", + requestId, (pTypeName ? pTypeName : "null"), (pBoundary ? pBoundary : "null")); + return setMultipartContentType(requestId, pTypeName, pBoundary); } s32 PS4_SYSV_ABI sceNpWebApiSetRequestTimeout(s64 requestId, u32 timeout) { 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 40a488a83..a682f8764 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 @@ -579,6 +579,94 @@ void checkRequestTimeout(OrbisNpWebApiRequest* request) { } } +s32 setMultipartContentType(s64 requestId, const char* pTypeName, const char* pBoundary) { + 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->multipartContentType = (pTypeName != nullptr) ? pTypeName : ""; + request->multipartBoundary = (pBoundary != nullptr) ? pBoundary : ""; + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + +s32 addMultipartPart(s64 requestId, const OrbisNpWebApiMultipartPartParameter* pParam, + s32* pIndex) { + if (pParam == nullptr) { + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + 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; + } + + // The library supplies Content-Length itself (from contentLength), so skip an app-provided one. + const auto isContentLength = [](const char* name) { + const char* t = "content-length"; + for (; *name != '\0' && *t != '\0'; ++name, ++t) { + char c = *name; + if (c >= 'A' && c <= 'Z') { + c = static_cast(c - 'A' + 'a'); + } + if (c != *t) { + return false; + } + } + return *name == '\0' && *t == '\0'; + }; + + OrbisNpWebApiRequest::MultipartPart part; + part.contentLength = pParam->contentLength; + std::string headers; + for (u64 i = 0; i < pParam->headerNum; ++i) { + const OrbisNpWebApiHttpHeader& h = pParam->pHeaders[i]; + if (h.pName == nullptr || h.pValue == nullptr || isContentLength(h.pName)) { + continue; + } + headers += h.pName; + headers += ": "; + headers += h.pValue; + headers += "\r\n"; + } + headers += "Content-Length: "; + headers += std::to_string(pParam->contentLength); + headers += "\r\n\r\n"; + part.rawHeaders = std::move(headers); + + request->multipartParts.push_back(std::move(part)); + if (pIndex != nullptr) { + *pIndex = static_cast(request->multipartParts.size()); // 1-based part index + } + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; +} + s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s8 flag, OrbisNpWebApiResponseInformationOption* pRespInfoOption) { OrbisNpWebApiContext* context = findAndValidateContext(requestId >> 0x30); @@ -601,7 +689,69 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s startRequestTimer(request); - // TODO: multipart logic + // Multipart requests accumulate each part payload across sendMultipartRequest() calls,the + // full multipart/mixed body is framed and transmitted once every declared part is complete. + std::string multipartBody; + const void* sendData = pData; + u64 sendSize = dataSize; + if (request->multipart) { + if (partIndex < 1 || partIndex > static_cast(request->multipartParts.size())) { + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + } + auto& part = request->multipartParts[partIndex - 1]; + if (pData != nullptr && dataSize > 0) { + part.data.append(static_cast(pData), dataSize); + } + part.sentSize += dataSize; + + bool allComplete = true; + for (const auto& p : request->multipartParts) { + if (p.sentSize < p.contentLength) { + allComplete = false; + break; + } + } + if (!allComplete) { + // Further part payloads still expected; nothing to transmit yet. + releaseRequest(request); + releaseUserContext(user_context); + releaseContext(context); + return ORBIS_OK; + } + + std::string boundary = request->multipartBoundary; + if (boundary.empty()) { + // Fallback boundary derived from the unique request id. + static const char kHex[] = "0123456789abcdef"; + boundary = "----shadPS4Boundary"; + const u64 v = static_cast(requestId); + for (int shift = 60; shift >= 0; shift -= 4) { + boundary += kHex[(v >> shift) & 0xf]; + } + } + const std::string typeName = request->multipartContentType.empty() + ? std::string("multipart/mixed") + : request->multipartContentType; + for (const auto& p : request->multipartParts) { + multipartBody += "--"; + multipartBody += boundary; + multipartBody += "\r\n"; + multipartBody += p.rawHeaders; // header lines + blank line separating headers/body + multipartBody += p.data; + multipartBody += "\r\n"; + } + multipartBody += "--"; + multipartBody += boundary; + multipartBody += "--\r\n"; + + request->userContentType = typeName + "; boundary=" + boundary; + request->userContentLength = multipartBody.size(); + sendData = multipartBody.data(); + sendSize = multipartBody.size(); + } if (g_sdk_ver >= Common::ElfInfo::FW_250 && !request->sent) { request->sent = true; @@ -723,8 +873,30 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s user_context->userId, request->userPath); } - // Replay app-supplied headers + // Replay app-supplied headers. Content-Type is already emitted above from the + // ContentParameter (userContentType),skip a duplicate app-supplied Content-Type so the + // request never carries two + const auto isContentType = [](const std::string& name) { + constexpr std::string_view target = "content-type"; + if (name.size() != target.size()) { + return false; + } + for (size_t i = 0; i < name.size(); ++i) { + char c = name[i]; + if (c >= 'A' && c <= 'Z') { + c = static_cast(c - 'A' + 'a'); + } + if (c != target[i]) { + return false; + } + } + return true; + }; + const bool haveContentType = !request->userContentType.empty(); for (const auto& [hname, hvalue] : request->userHeaders) { + if (haveContentType && isContentType(hname)) { + continue; + } Libraries::Http::sceHttpAddRequestHeader(req_id, hname.c_str(), hvalue.c_str(), /*mode=*/0); } @@ -733,7 +905,7 @@ s32 sendRequest(s64 requestId, s32 partIndex, const void* pData, u64 dataSize, s setRequestState(request, 4); const s32 send_err = - Libraries::Http::sceHttpSendRequest(request->http_request_id, pData, dataSize); + Libraries::Http::sceHttpSendRequest(request->http_request_id, sendData, sendSize); if (send_err < 0) { LOG_ERROR(Lib_NpWebApi, "sendRequest: sceHttpSendRequest failed: {:#x}", send_err); releaseRequest(request); 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 7ccf6ef28..6136f8702 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 @@ -70,6 +70,17 @@ struct OrbisNpWebApiRequest { // the libSceHttp request in the send path after the lib-managed CT/Authorization. std::vector> userHeaders; bool multipart; + // Multipart body assembly (sceNpWebApiSetMultipartContentType / AddMultipartPart / + // SendMultipartRequest). Parts accumulate across sends,the body is framed once all are in. + struct MultipartPart { + std::string rawHeaders; // ": \r\n...Content-Length: \r\n\r\n" + u64 contentLength = 0; + std::string data; + u64 sentSize = 0; + }; + std::string multipartContentType; // type name, e.g. "multipart/mixed" + std::string multipartBoundary; + std::vector multipartParts; bool aborted; bool sent; u32 requestTimeout; @@ -203,6 +214,8 @@ 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, OrbisNpWebApiResponseInformationOption* pResponseInformationOption); // FUN_01001c50 +s32 setMultipartContentType(s64 requestId, const char* pTypeName, const char* pBoundary); +s32 addMultipartPart(s64 requestId, const OrbisNpWebApiMultipartPartParameter* pParam, s32* pIndex); s32 abortRequestInternal(OrbisNpWebApiContext* context, OrbisNpWebApiUserContext* userContext, OrbisNpWebApiRequest* request); // FUN_01001b70 s32 abortRequest(s64 requestId); // FUN_01002c70