Implemented MultipartPart content for npwebapi (#4663)

* implemented MultipartPart content

* fixed possible duplicate content-type resubmit
This commit is contained in:
georgemoralis 2026-07-03 17:09:49 +03:00 committed by GitHub
parent 5b29e8cd95
commit d460c94b60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 194 additions and 17 deletions

View File

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

View File

@ -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<char>(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<s32>(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<s32>(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<const char*>(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<u64>(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<char>(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);

View File

@ -70,6 +70,17 @@ struct OrbisNpWebApiRequest {
// the libSceHttp request in the send path after the lib-managed CT/Authorization.
std::vector<std::pair<std::string, std::string>> 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; // "<name>: <value>\r\n...Content-Length: <n>\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<MultipartPart> 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