diff --git a/src/core/libraries/network/http2.cpp b/src/core/libraries/network/http2.cpp index 830a6e8f9..f53e2b020 100644 --- a/src/core/libraries/network/http2.cpp +++ b/src/core/libraries/network/http2.cpp @@ -18,7 +18,8 @@ s32 PS4_SYSV_ABI sceHttp2AddCookie() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceHttp2AddRequestHeader() { +s32 PS4_SYSV_ABI sceHttp2AddRequestHeader(s32 template_or_req_id, const char* name, + const char* value, u32 mode) { LOG_ERROR(Lib_Http2, "(STUBBED) called"); return ORBIS_OK; } @@ -48,12 +49,13 @@ s32 PS4_SYSV_ABI sceHttp2CreateCookieBox() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceHttp2CreateRequestWithURL() { +s32 PS4_SYSV_ABI sceHttp2CreateRequestWithURL(s32 tmpl_id, const char* method, const char* url, + u64 content_length) { LOG_ERROR(Lib_Http2, "(STUBBED) called"); return ORBIS_OK; } -s32 PS4_SYSV_ABI sceHttp2CreateTemplate(s32 ctx_id, char* user_agent, s32 http_ver, +s32 PS4_SYSV_ABI sceHttp2CreateTemplate(s32 ctx_id, const char* user_agent, s32 http_ver, s32 auto_proxy_conf) { LOG_ERROR(Lib_Http2, "(STUBBED) called"); return ORBIS_OK; @@ -74,7 +76,7 @@ s32 PS4_SYSV_ABI sceHttp2DeleteTemplate() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceHttp2GetAllResponseHeaders() { +s32 PS4_SYSV_ABI sceHttp2GetAllResponseHeaders(s32 req_id, char** header, u64* header_size) { LOG_ERROR(Lib_Http2, "(STUBBED) called"); return ORBIS_OK; } @@ -114,7 +116,7 @@ s32 PS4_SYSV_ABI sceHttp2GetResponseContentLength() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceHttp2GetStatusCode() { +s32 PS4_SYSV_ABI sceHttp2GetStatusCode(s32 request_id, s32* status_code) { LOG_ERROR(Lib_Http2, "(STUBBED) called"); return ORBIS_OK; } @@ -145,7 +147,7 @@ s32 PS4_SYSV_ABI sceHttp2RemoveRequestHeader() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceHttp2SendRequest() { +s32 PS4_SYSV_ABI sceHttp2SendRequest(s32 req_id, const void* data, u64 size) { LOG_ERROR(Lib_Http2, "(STUBBED) called"); return ORBIS_OK; } @@ -236,7 +238,7 @@ s32 PS4_SYSV_ABI sceHttp2SetRedirectCallback() { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceHttp2SetRequestContentLength() { +s32 PS4_SYSV_ABI sceHttp2SetRequestContentLength(s32 req_id, u64 content_length) { LOG_ERROR(Lib_Http2, "(STUBBED) called"); return ORBIS_OK; } diff --git a/src/core/libraries/network/http2.h b/src/core/libraries/network/http2.h index daf1ad3a7..456726820 100644 --- a/src/core/libraries/network/http2.h +++ b/src/core/libraries/network/http2.h @@ -29,19 +29,21 @@ using OrbisHttp2PreSendCallback = PS4_SYSV_ABI s32 (*)(s32 request_id, s32 ssl_i s32 PS4_SYSV_ABI sceHttp2AbortRequest(); s32 PS4_SYSV_ABI sceHttp2AddCookie(); -s32 PS4_SYSV_ABI sceHttp2AddRequestHeader(); +s32 PS4_SYSV_ABI sceHttp2AddRequestHeader(s32 template_or_req_id, const char* name, + const char* value, u32 mode); s32 PS4_SYSV_ABI sceHttp2AuthCacheFlush(); s32 PS4_SYSV_ABI sceHttp2CookieExport(); s32 PS4_SYSV_ABI sceHttp2CookieFlush(); s32 PS4_SYSV_ABI sceHttp2CookieImport(); s32 PS4_SYSV_ABI sceHttp2CreateCookieBox(); -s32 PS4_SYSV_ABI sceHttp2CreateRequestWithURL(); -s32 PS4_SYSV_ABI sceHttp2CreateTemplate(s32 ctx_id, char* user_agent, s32 http_ver, +s32 PS4_SYSV_ABI sceHttp2CreateRequestWithURL(s32 tmpl_id, const char* method, const char* url, + u64 content_length); +s32 PS4_SYSV_ABI sceHttp2CreateTemplate(s32 ctx_id, const char* user_agent, s32 http_ver, s32 auto_proxy_conf); s32 PS4_SYSV_ABI sceHttp2DeleteCookieBox(); s32 PS4_SYSV_ABI sceHttp2DeleteRequest(); s32 PS4_SYSV_ABI sceHttp2DeleteTemplate(); -s32 PS4_SYSV_ABI sceHttp2GetAllResponseHeaders(); +s32 PS4_SYSV_ABI sceHttp2GetAllResponseHeaders(s32 req_id, char** header, u64* header_size); s32 PS4_SYSV_ABI sceHttp2GetAuthEnabled(); s32 PS4_SYSV_ABI sceHttp2GetAutoRedirect(); s32 PS4_SYSV_ABI sceHttp2GetCookie(); @@ -49,13 +51,13 @@ s32 PS4_SYSV_ABI sceHttp2GetCookieBox(); s32 PS4_SYSV_ABI sceHttp2GetCookieStats(); s32 PS4_SYSV_ABI sceHttp2GetMemoryPoolStats(); s32 PS4_SYSV_ABI sceHttp2GetResponseContentLength(); -s32 PS4_SYSV_ABI sceHttp2GetStatusCode(); +s32 PS4_SYSV_ABI sceHttp2GetStatusCode(s32 request_id, s32* status_code); s32 PS4_SYSV_ABI sceHttp2Init(s32 net_id, s32 ssl_id, u64 pool_size, s32 max_requests); s32 PS4_SYSV_ABI sceHttp2ReadData(); s32 PS4_SYSV_ABI sceHttp2ReadDataAsync(); s32 PS4_SYSV_ABI sceHttp2RedirectCacheFlush(); s32 PS4_SYSV_ABI sceHttp2RemoveRequestHeader(); -s32 PS4_SYSV_ABI sceHttp2SendRequest(); +s32 PS4_SYSV_ABI sceHttp2SendRequest(s32 req_id, const void* data, u64 size); s32 PS4_SYSV_ABI sceHttp2SendRequestAsync(); s32 PS4_SYSV_ABI sceHttp2SetAuthEnabled(); s32 PS4_SYSV_ABI sceHttp2SetAuthInfoCallback(); @@ -74,7 +76,7 @@ s32 PS4_SYSV_ABI sceHttp2SetPreSendCallback(s32 template_id, OrbisHttp2PreSendCa void* user_arg); s32 PS4_SYSV_ABI sceHttp2SetRecvTimeOut(); s32 PS4_SYSV_ABI sceHttp2SetRedirectCallback(); -s32 PS4_SYSV_ABI sceHttp2SetRequestContentLength(); +s32 PS4_SYSV_ABI sceHttp2SetRequestContentLength(s32 req_id, u64 content_length); s32 PS4_SYSV_ABI sceHttp2SetResolveRetry(); s32 PS4_SYSV_ABI sceHttp2SetResolveTimeOut(); s32 PS4_SYSV_ABI sceHttp2SetSendTimeOut(); diff --git a/src/core/libraries/np/np_web_api2/np_web_api2.cpp b/src/core/libraries/np/np_web_api2/np_web_api2.cpp index ba9d1c43d..bd0390154 100644 --- a/src/core/libraries/np/np_web_api2/np_web_api2.cpp +++ b/src/core/libraries/np/np_web_api2/np_web_api2.cpp @@ -277,18 +277,24 @@ s32 PS4_SYSV_ABI sceNpWebApi2ReadData(s64 request_id, void* data, u64 size) { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceNpWebApi2SendMultipartRequest() { - LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI +sceNpWebApi2SendMultipartRequest(s64 request_id, s32 part_index, void* data, u64 data_size, + OrbisNpWebApi2ResponseInformationOption* resp_info_option) { + if (part_index <= 0 || !data || data_size == 0) { + LOG_ERROR(Lib_NpWebApi2, "Invalid parameters"); + return ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT; + } + + LOG_INFO(Lib_NpWebApi2, "called, request_id = {:#x}, part_index = {}, data_size = {:#x}", + request_id, part_index, data_size); + return sendRequest(request_id, part_index, data, data_size, resp_info_option); } -s32 PS4_SYSV_ABI sceNpWebApi2SendRequest() { - if (!EmulatorSettings.IsShadNetEnabled()) { - LOG_INFO(Lib_NpWebApi2, "called, returning shadNet signed out."); - return ORBIS_NP_WEBAPI2_ERROR_NOT_SIGNED_IN; - } - LOG_ERROR(Lib_NpWebApi2, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI +sceNpWebApi2SendRequest(s64 request_id, void* data, u64 data_size, + OrbisNpWebApi2ResponseInformationOption* resp_info_option) { + LOG_INFO(Lib_NpWebApi2, "called, request_id = {:#x}, data_size = {:#x}", request_id, data_size); + return sendRequest(request_id, 0, data, data_size, resp_info_option); } s32 PS4_SYSV_ABI sceNpWebApi2SetMultipartContentType() { diff --git a/src/core/libraries/np/np_web_api2/np_web_api2.h b/src/core/libraries/np/np_web_api2/np_web_api2.h index d016001a2..7d2cbf142 100644 --- a/src/core/libraries/np/np_web_api2/np_web_api2.h +++ b/src/core/libraries/np/np_web_api2/np_web_api2.h @@ -42,5 +42,13 @@ struct OrbisNpWebApi2ContentParameter { u8 reserved[16]; }; +struct OrbisNpWebApi2ResponseInformationOption { + s32 http_status; + s32 reserved; + char* error_object; + u64 error_object_size; + u64 response_data_size; +}; + void RegisterLib(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Np::NpWebApi2 \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp b/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp index c2c753714..32eedbaee 100644 --- a/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp +++ b/src/core/libraries/np/np_web_api2/np_web_api2_context.cpp @@ -5,6 +5,7 @@ #include "core/libraries/network/http2.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_error.h" +#include "core/libraries/np/np_handler.h" #include "core/libraries/np/np_web_api2/np_web_api2_context.h" namespace Libraries::Np::NpWebApi2 { @@ -144,7 +145,7 @@ Request* UserContext::GetRequest(s64 request_id) { return request; } -bool IsInternalHeader(const char* header_name) { +static bool IsInternalHeader(const char* header_name) { std::string lower_name{header_name}; std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), [](char c) { return std::tolower(c); }); @@ -177,4 +178,98 @@ s32 Request::AddHttpRequestHeader(const char* field_name, const char* field_valu return ORBIS_OK; } +s32 Request::CreateHttpRequest(s32 http_template_id, const char* url) { + const char* method = this->method.empty() ? "UNKNOWN" : this->method.data(); + s32 http_request_id = Libraries::Http2::sceHttp2CreateRequestWithURL(http_template_id, method, + url, this->content_length); + if (http_request_id < 0) { + LOG_ERROR(Lib_NpWebApi2, "Failed to create Http2 request, error = {:#x}", http_request_id); + return http_request_id; + } + this->http_request_id = http_request_id; + s32 result = Libraries::Http2::sceHttp2AddRequestHeader(http_request_id, "Content-Type", + this->content_type.data(), 0); + if (result < 0) { + LOG_ERROR(Lib_NpWebApi2, "Failed to add Content-Type request header, error = {:#x}", + result); + return result; + } + + UserContext* user_ctx = this->parent_ctx->GetUserContext(this->id >> 0x20); + const std::string bearer = NpHandler::GetInstance().GetBearerToken(user_ctx->GetUserId()); + if (!bearer.empty()) { + const std::string auth_value = "Bearer " + bearer; + result = Libraries::Http2::sceHttp2AddRequestHeader(http_request_id, "Authorization", + auth_value.data(), 0); + } + if (bearer.empty() || result < 0) { + LOG_WARNING(Lib_NpWebApi2, "Failed to add Authorization request header"); + } + + // This would go on to add several other PSN-specific headers. + // As shadNet does not appear to use them, the logic is skipped for now. + + for (HttpRequestHeader* header : this->http_headers) { + s32 result = Libraries::Http2::sceHttp2AddRequestHeader( + http_request_id, header->field_name.data(), header->field_value.data(), 0); + if (result < 0) { + LOG_ERROR(Lib_NpWebApi2, + "Failed to add request header, name = {}, value = {}, error = {:#x}", + header->field_name.data(), header->field_value.data(), result); + user_ctx->RemoveUser(); + return result; + } + } + user_ctx->RemoveUser(); + return result; +} + +s32 Request::SendHttpRequest(void* data, u64 data_size) { + s32 result = 0; + if (this->content_length != 0 && this->sent_data == 0) { + // Real library seems to do some calculations, some parts including IPC calls. + // Not entirely sure how accurate this is in practice. + result = Libraries::Http2::sceHttp2SetRequestContentLength(this->http_request_id, + this->content_length); + if (result < 0) { + LOG_ERROR(Lib_NpWebApi2, "Failed to set content length, error = {:#x}", result); + return result; + } + } + if (!data || data_size == 0 || this->content_length == 0) { + result = Libraries::Http2::sceHttp2SendRequest(this->http_request_id, nullptr, 0); + if (result < 0) { + LOG_ERROR(Lib_NpWebApi2, "Failed to send request, error = {:#x}", result); + return result; + } + } + + u64 actual_size = std::min(data_size, this->content_length - this->sent_data); + if (this->content_length <= this->sent_data) { + // Nothing left to send + return result; + } + result = Libraries::Http2::sceHttp2SendRequest(this->http_request_id, data, actual_size); + if (result < 0) { + LOG_ERROR(Lib_NpWebApi2, "Failed to send request, error = {:#x}", result); + return result; + } + this->sent_data += actual_size; + return result; +} + +s32 Request::GetAllHttpResponseHeaders() { + char* header = nullptr; + u64 size = 0; + s32 result = + Libraries::Http2::sceHttp2GetAllResponseHeaders(this->http_request_id, &header, &size); + if (result < 0) { + LOG_ERROR(Lib_NpWebApi2, "Failed to get response headers, error = {:#x}", result); + return result; + } + this->http_response_headers = header; + this->http_response_header_size = size; + return result; +} + }; // namespace Libraries::Np::NpWebApi2 \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api2/np_web_api2_context.h b/src/core/libraries/np/np_web_api2/np_web_api2_context.h index 09ee2f282..551102bcd 100644 --- a/src/core/libraries/np/np_web_api2/np_web_api2_context.h +++ b/src/core/libraries/np/np_web_api2/np_web_api2_context.h @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "core/libraries/kernel/time.h" #include "core/libraries/np/np_web_api2/np_web_api2.h" #include "core/libraries/np/np_web_api2/np_web_api2_push_event.h" #include "core/libraries/system/userservice.h" @@ -137,6 +138,10 @@ public: return user_id; } + s32 GetHttpTemplateId() { + return http_template_id; + } + s32 Initialize(); s32 CreateRequest(const char* api_group, const char* path, const char* method, @@ -164,7 +169,8 @@ public: const char* method, bool multipart, const OrbisNpWebApi2ContentParameter* content_parameter) : parent_ctx(parent), id(request_id), api_group(api_group), path(path), method(method), - multipart_supported(multipart), content_type(content_parameter->content_type) {} + multipart_supported(multipart), content_type(content_parameter->content_type), + content_length(content_parameter->content_length) {} void Lock() { parent_ctx->Lock(); @@ -190,23 +196,84 @@ public: return id; } + s32 GetHttpRequestId() { + return http_request_id; + } + + bool IsMultipart() { + return multipart_supported; + } + bool HasSent() { return sent; } + void MarkSent() { + sent = true; + } + + bool Aborted() { + return aborted; + } + + bool Expired() { + return expired; + } + + bool OutOfData() { + return content_length == sent_data; + } + + void SetEndTime() { + if (timeout != 0 && end_time == 0) { + end_time = Libraries::Kernel::sceKernelGetProcessTime() + timeout; + } + } + + void ClearEndTime() { + end_time = 0; + } + + std::string& GetApiGroup() { + return api_group; + } + + std::string& GetPath() { + return path; + } + + std::string& GetMethod() { + return method; + } + + std::string& GetContentType() { + return content_type; + } + s32 AddHttpRequestHeader(const char* field_name, const char* field_value); + s32 CreateHttpRequest(s32 http_template_id, const char* url); + s32 SendHttpRequest(void* data, u64 data_size); + s32 GetAllHttpResponseHeaders(); private: s64 id{}; + u64 content_length{}; + char* http_response_headers{}; + u64 http_response_header_size{}; s32 user_count{}; s32 http_request_id{}; + s32 timeout{}; + s32 sent_data{}; + bool multipart_supported{}; + bool sent{}; + bool aborted{}; + bool expired{}; + u64 end_time{}; LibraryContext* parent_ctx{}; std::string api_group{}; std::string path{}; std::string method{}; std::string content_type{}; - bool multipart_supported{}; - bool sent{}; struct HttpRequestHeader { std::string field_name; diff --git a/src/core/libraries/np/np_web_api2/np_web_api2_internal.cpp b/src/core/libraries/np/np_web_api2/np_web_api2_internal.cpp index 27c1352cc..94c4032de 100644 --- a/src/core/libraries/np/np_web_api2/np_web_api2_internal.cpp +++ b/src/core/libraries/np/np_web_api2/np_web_api2_internal.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/logging/log.h" +#include "core/emulator_settings.h" #include "core/libraries/network/http2.h" #include "core/libraries/np/np_error.h" #include "core/libraries/np/np_web_api2/np_web_api2_context.h" @@ -185,4 +186,145 @@ s32 addHttpRequestHeader(s64 request_id, const char* field_name, const char* fie return result; } +static s32 checkRequestStatus(Request* request, UserContext* user_ctx, LibraryContext* lib_ctx) { + if (request->Expired()) { + LOG_ERROR(Lib_NpWebApi2, "Request timed out"); + request->RemoveUser(); + user_ctx->RemoveUser(); + lib_ctx->RemoveUser(); + return ORBIS_NP_WEBAPI2_ERROR_TIMEOUT; + } + + if (request->Aborted()) { + LOG_ERROR(Lib_NpWebApi2, "Request aborted"); + request->RemoveUser(); + user_ctx->RemoveUser(); + lib_ctx->RemoveUser(); + return ORBIS_NP_WEBAPI2_ERROR_ABORTED; + } + return ORBIS_OK; +} + +s32 sendRequest(s64 request_id, s32 part_index, void* data, u64 data_size, + OrbisNpWebApi2ResponseInformationOption* resp_info_option) { + s32 lib_ctx_id = static_cast(request_id >> 0x30); + s32 user_ctx_id = static_cast(request_id >> 0x20); + LibraryContext* lib_ctx = getLibraryContext(lib_ctx_id); + if (!lib_ctx) { + LOG_ERROR(Lib_NpWebApi2, "No library context for request id {:#x}", request_id); + return ORBIS_NP_WEBAPI2_ERROR_LIB_CONTEXT_NOT_FOUND; + } + + UserContext* user_ctx = lib_ctx->GetUserContext(user_ctx_id); + if (!user_ctx) { + LOG_ERROR(Lib_NpWebApi2, "No user context for request id {:#x}", request_id); + lib_ctx->RemoveUser(); + return ORBIS_NP_WEBAPI2_ERROR_USER_CONTEXT_NOT_FOUND; + } + + Request* request = user_ctx->GetRequest(request_id); + if (!request) { + LOG_ERROR(Lib_NpWebApi2, "No request with id {:#x}", request_id); + user_ctx->RemoveUser(); + lib_ctx->RemoveUser(); + return ORBIS_NP_WEBAPI2_ERROR_REQUEST_NOT_FOUND; + } + + request->SetEndTime(); + if (request->IsMultipart()) { + if (part_index == 0) { + LOG_ERROR(Lib_NpWebApi2, "part_index 0 is prohibited for multipart requests"); + request->RemoveUser(); + user_ctx->RemoveUser(); + lib_ctx->RemoveUser(); + return ORBIS_NP_WEBAPI2_ERROR_PROHIBITED_FUNCTION_CALL; + } + // TODO: Multipart logic + } + + if (!request->HasSent()) { + request->MarkSent(); + } + + request->Lock(); + s32 result = checkRequestStatus(request, user_ctx, lib_ctx); + request->Unlock(); + if (result != 0) { + // Aborted or timed out. + return result; + } + + if (!EmulatorSettings.IsShadNetEnabled()) { + LOG_INFO(Lib_NpWebApi2, "Cannot send request, you are not signed in to shadNet"); + request->RemoveUser(); + user_ctx->RemoveUser(); + lib_ctx->RemoveUser(); + return ORBIS_NP_WEBAPI2_ERROR_NOT_SIGNED_IN; + } + + if (request->GetHttpRequestId() == 0) { + std::string base_url = EmulatorSettings.GetShadNetWebApiServer(); + s32 template_id = user_ctx->GetHttpTemplateId(); + // Technically this should be base_url + api_group + path, + // but shadNet doesn't seem to use the api_group for endpoints? + std::string full_url = base_url + request->GetPath(); + result = request->CreateHttpRequest(template_id, full_url.data()); + if (result < 0) { + // Underlying request creation failed, return. + request->RemoveUser(); + user_ctx->RemoveUser(); + lib_ctx->RemoveUser(); + return result; + } + } + + request->Lock(); + result = checkRequestStatus(request, user_ctx, lib_ctx); + request->Unlock(); + if (result != 0) { + // Aborted or timed out. + return result; + } + + if (request->IsMultipart()) { + // TODO: Multipart sends + // result = request->SendMultipartHttpRequest(data, data_size, part_index); + } else { + result = request->SendHttpRequest(data, data_size); + } + + if (result >= 0 && request->OutOfData()) { + result = request->GetAllHttpResponseHeaders(); + if (result >= 0) { + s32 http_request_id = request->GetHttpRequestId(); + s32 http_status{}; + result = Libraries::Http2::sceHttp2GetStatusCode(http_request_id, &http_status); + if (result >= 0) { + if (resp_info_option) { + resp_info_option->http_status = http_status; + } + if (http_status < 400) { + result = http_status | 0x82f00000; + } + } else { + LOG_ERROR(Lib_NpWebApi2, "Failed to get http status code, error = {:#x}", result); + } + } + } + + request->Lock(); + s32 result2 = checkRequestStatus(request, user_ctx, lib_ctx); + request->Unlock(); + if (result2 != 0) { + // Aborted or timed out. + return result2; + } + + request->ClearEndTime(); + request->RemoveUser(); + user_ctx->RemoveUser(); + lib_ctx->RemoveUser(); + return result; +} + }; // namespace Libraries::Np::NpWebApi2 \ No newline at end of file diff --git a/src/core/libraries/np/np_web_api2/np_web_api2_internal.h b/src/core/libraries/np/np_web_api2/np_web_api2_internal.h index 147816dbb..3e1648eaa 100644 --- a/src/core/libraries/np/np_web_api2/np_web_api2_internal.h +++ b/src/core/libraries/np/np_web_api2/np_web_api2_internal.h @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "core/libraries/np/np_web_api2/np_web_api2.h" #include "core/libraries/system/userservice.h" namespace Libraries::Np::NpWebApi2 { @@ -12,4 +13,6 @@ s32 createRequest(s32 user_ctx_id, const char* api_group, const char* path, cons const OrbisNpWebApi2ContentParameter* content_parameter, bool multipart, s64* request_id); s32 addHttpRequestHeader(s64 request_id, const char* field_name, const char* field_value); +s32 sendRequest(s64 request_id, s32 part_index, void* data, u64 data_size, + OrbisNpWebApi2ResponseInformationOption* resp_info_option); }; // namespace Libraries::Np::NpWebApi2 \ No newline at end of file