From 4fbb07618b706f28d2a558a6ce963aa03533deb4 Mon Sep 17 00:00:00 2001 From: Aaditya Bhusal Date: Mon, 12 Oct 2026 08:47:02 +0300 Subject: [PATCH 1/4] feat(http): model network handles and request state --- core/libs/prx/libSceHttp/src/HttpErrors.hpp | 8 + core/libs/prx/libSceHttp/src/Network.cpp | 394 ++++++++++++++++++++ core/libs/prx/libSceHttp/src/Network.hpp | 39 ++ core/libs/prx/libSceHttp/src/Transport.hpp | 47 +++ 4 files changed, 488 insertions(+) create mode 100644 core/libs/prx/libSceHttp/src/Network.cpp create mode 100644 core/libs/prx/libSceHttp/src/Network.hpp create mode 100644 core/libs/prx/libSceHttp/src/Transport.hpp diff --git a/core/libs/prx/libSceHttp/src/HttpErrors.hpp b/core/libs/prx/libSceHttp/src/HttpErrors.hpp index 1b18ec3e1e8..14814f809e9 100644 --- a/core/libs/prx/libSceHttp/src/HttpErrors.hpp +++ b/core/libs/prx/libSceHttp/src/HttpErrors.hpp @@ -8,6 +8,14 @@ inline constexpr int ERROR_ABORTED = static_cast(0x80431080); inline constexpr int ERROR_INVALID_ID = static_cast(0x80431100); inline constexpr int ERROR_INVALID_VALUE = static_cast(0x804311FE); inline constexpr int ERROR_INVALID_URL = static_cast(0x80433060); +inline constexpr int ERROR_HTTP_BUSY = static_cast(0x80431021); +inline constexpr int ERROR_UNKNOWN_METHOD = static_cast(0x8043106b); +inline constexpr int ERROR_BEFORE_SEND = static_cast(0x80431065); +inline constexpr int ERROR_AFTER_SEND = static_cast(0x80431066); +inline constexpr int ERROR_HTTP_TIMEOUT = static_cast(0x80431068); +inline constexpr int ERROR_HTTP_HEADER_TOO_LARGE = static_cast(0x80431073); +inline constexpr int ERROR_HTTP_SSL = static_cast(0x80431075); +inline constexpr int ERROR_HTTP_ABORTED = static_cast(0x80431080); inline constexpr int ERROR_PARSE_HTTP_INVALID_RESPONSE = static_cast(0x80432060); inline constexpr int ERROR_PARSE_HTTP_INVALID_VALUE = static_cast(0x804321FE); diff --git a/core/libs/prx/libSceHttp/src/Network.cpp b/core/libs/prx/libSceHttp/src/Network.cpp new file mode 100644 index 00000000000..342a5aa2b0f --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Network.cpp @@ -0,0 +1,394 @@ +#include "Network.hpp" +#include "Transport.hpp" +#include "HttpErrors.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); + +namespace HttpNetwork { +namespace { + +struct Node { + Kind kind; + int parent; + Settings settings; + std::string url; + std::string method; + std::uint64_t contentLength = 0; + std::mutex mutex; + std::mutex operation; + std::shared_ptr transport; + bool deleted = false; + bool aborted = false; + bool sent = false; + bool ready = false; + int result = ERROR_BEFORE_SEND; +}; + +std::mutex registryMutex; +std::map> registry; +int nextHandle = 1; + +std::shared_ptr find(int id) { + std::lock_guard lock(registryMutex); + const auto item = registry.find(id); + return item == registry.end() ? nullptr : item->second; +} + +int insert(Kind kind, int parent, Settings settings, std::string url = {}, std::string method = {}, std::uint64_t length = 0) { + auto node = std::make_shared(); + node->kind = kind; + node->parent = parent; + node->settings = std::move(settings); + node->url = std::move(url); + node->method = std::move(method); + node->contentLength = length; + std::lock_guard lock(registryMutex); + if (parent != 0 && !registry.contains(parent)) return ERROR_INVALID_ID; + if (nextHandle == std::numeric_limits::max()) return ERROR_OUT_OF_MEMORY; + const auto id = nextHandle++; + registry.emplace(id, std::move(node)); + return id; +} + +char lower(char value) { + return value >= 'A' && value <= 'Z' ? value + ('a' - 'A') : value; +} + +bool equal(std::string_view left, std::string_view right) { + return left.size() == right.size() && std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) { return lower(a) == lower(b); }); +} + +bool token(std::string_view value) { + return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || std::string_view("!#$%&'*+-.^_`|~").find(c) != std::string_view::npos; + }); +} + +int validateUrl(const char* url, std::string* origin = nullptr) { + if (!url) return ERROR_INVALID_URL; + std::size_t size = 0; + auto result = sceHttpUriParse(nullptr, url, nullptr, &size, 0); + if (result != 0) return result; + std::vector pool(size); + SceHttpUriElement parts{}; + result = sceHttpUriParse(&parts, url, pool.data(), nullptr, pool.size()); + if (result != 0) return result; + if ((!equal(parts.scheme, "http") && !equal(parts.scheme, "https")) || parts.opaque || parts.hostname[0] == '\0') return ERROR_INVALID_URL; + if (parts.username[0] || parts.password[0]) return ERROR_INVALID_URL; + if (origin) { + *origin = equal(parts.scheme, "https") ? "https://" : "http://"; + const std::string_view host(parts.hostname); + if (host.find(':') != std::string_view::npos) *origin += '['; + origin->append(host); + if (host.find(':') != std::string_view::npos) *origin += ']'; + *origin += ':' + std::to_string(parts.port); + } + return 0; +} + +} + +bool Enabled_nid_no_patch() { + static const bool enabled = [] { + const char* value = std::getenv("APS5_HTTP_NETWORK"); + if (!value || !*value || std::string_view(value) == "0") return false; + if (std::string_view(value) != "1") throw std::runtime_error("APS5_HTTP_NETWORK: expected 0 or 1"); +#ifndef _WIN32 + throw std::runtime_error("APS5_HTTP_NETWORK: host HTTP is only implemented on Windows"); +#endif + return true; + }(); + return enabled; +} + +int Init_nid_no_patch(std::uint64_t poolSize) { + if (poolSize == 0) return ERROR_INVALID_VALUE; + return insert(Kind::Context, 0, {}); +} + +int Template_nid_no_patch(int context, const char* agent, int version, int proxy) { + auto parent = find(context); + if (!parent || parent->kind != Kind::Context) return ERROR_INVALID_ID; + if (!agent || (version != 1 && version != 2) || static_cast(proxy) > 1) return ERROR_INVALID_VALUE; + if (proxy) NotImplemented_nid_no_patch("sceHttpCreateTemplate: automatic proxy configuration"); + std::lock_guard lock(parent->mutex); + auto settings = parent->settings; + settings.agent = agent; + settings.version = version; + return insert(Kind::Template, context, std::move(settings)); +} + +int ConnectionUrl_nid_no_patch(int id, const char* url, int keepAlive) { + auto parent = find(id); + if (!parent || parent->kind != Kind::Template) return ERROR_INVALID_ID; + if (static_cast(keepAlive) > 1) return ERROR_INVALID_VALUE; + std::string origin; + const auto result = validateUrl(url, &origin); + if (result != 0) return result; + std::lock_guard lock(parent->mutex); + auto settings = parent->settings; + settings.keepAlive = keepAlive != 0; + return insert(Kind::Connection, id, std::move(settings), std::move(origin)); +} + +int ConnectionHost_nid_no_patch(int id, const char* host, const char* scheme, std::uint16_t port, int keepAlive) { + if (!host || !scheme) return ERROR_INVALID_VALUE; + std::string url = std::string(scheme) + "://"; + const std::string_view name(host); + if (name.find(':') != std::string_view::npos && !name.starts_with('[')) url += '['; + url += host; + if (name.find(':') != std::string_view::npos && !name.starts_with('[')) url += ']'; + if (port != 0) url += ':' + std::to_string(port); + return ConnectionUrl_nid_no_patch(id, url.c_str(), keepAlive); +} + +int RequestMethod_nid_no_patch(int id, const char* method, const char* path, std::uint64_t length, bool absolute) { + auto parent = find(id); + if (!parent || parent->kind != Kind::Connection) return ERROR_INVALID_ID; + if (!method || !token(method) || !path) return ERROR_INVALID_VALUE; + if (equal(method, "CONNECT")) NotImplemented_nid_no_patch("sceHttpCreateRequest: CONNECT tunnels"); + std::lock_guard lock(parent->mutex); + const auto url = absolute ? std::string(path) : parent->url + (path[0] == '/' ? "" : "/") + path; + const auto result = validateUrl(url.c_str()); + if (result != 0) return result; + return insert(Kind::Request, id, parent->settings, url, method, length); +} + +int RequestIndex_nid_no_patch(int id, int method, const char* path, std::uint64_t length, bool absolute) { + constexpr const char* methods[] = {"GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE", "CONNECT"}; + if (method < 0 || method >= 8) return ERROR_UNKNOWN_METHOD; + return RequestMethod_nid_no_patch(id, methods[method], path, length, absolute); +} + +int Delete_nid_no_patch(int id, Kind kind) { + std::vector> removed; + { + std::lock_guard lock(registryMutex); + const auto item = registry.find(id); + if (item == registry.end() || item->second->kind != kind) return ERROR_INVALID_ID; + std::vector ids{id}; + for (std::size_t i = 0; i < ids.size(); ++i) { + for (const auto& [child, node] : registry) if (node->parent == ids[i]) ids.push_back(child); + } + for (const auto child : ids) { + removed.push_back(registry.at(child)); + registry.erase(child); + } + } + for (const auto& node : removed) { + std::shared_ptr transport; + { + std::lock_guard lock(node->mutex); + node->deleted = true; + node->aborted = true; + transport = node->transport; + } + if (transport) transport->Abort(); + } + return 0; +} + +int Configure_nid_no_patch(int id, Option option, std::uint64_t value) { + const auto node = find(id); + if (!node) return ERROR_INVALID_ID; + std::lock_guard lock(node->mutex); + if (node->deleted) return ERROR_INVALID_ID; + if (node->sent) return ERROR_AFTER_SEND; + auto& settings = node->settings; + switch (option) { + case Option::Redirect: + case Option::Gzip: + if (value > 1) return ERROR_INVALID_VALUE; + if (option == Option::Redirect) settings.redirect = value != 0; + if (option == Option::Gzip) settings.gzip = value != 0; + break; + case Option::ResolveTimeout: settings.resolveTimeout = static_cast(value); break; + case Option::ConnectTimeout: settings.connectTimeout = static_cast(value); break; + case Option::SendTimeout: settings.sendTimeout = static_cast(value); break; + case Option::ReceiveTimeout: settings.receiveTimeout = static_cast(value); break; + case Option::HeaderLimit: + if (value == 0 || value > std::numeric_limits::max()) return ERROR_INVALID_VALUE; + settings.headerLimit = value; + break; + case Option::ContentLength: + if (node->kind != Kind::Request) return ERROR_INVALID_ID; + node->contentLength = value; + break; + case Option::ReceiveBlockSize: + if (value == 0 || value > static_cast(std::numeric_limits::max())) return ERROR_INVALID_VALUE; + settings.receiveBlockSize = static_cast(value); + break; + } + return 0; +} + +int SslOptions_nid_no_patch(int id, std::uint32_t flags, bool enable) { + const auto node = find(id); + if (!node) return ERROR_INVALID_ID; + if (flags & ~0xffu) return ERROR_INVALID_VALUE; + if (flags & 0xc2u) NotImplemented_nid_no_patch("sceHttpsEnableOption: client verification, session reuse or SNI control"); + std::lock_guard lock(node->mutex); + if (node->deleted) return ERROR_INVALID_ID; + if (node->sent) return ERROR_AFTER_SEND; + const auto proposed = enable ? node->settings.sslFlags | flags : node->settings.sslFlags & ~flags; + if ((proposed & 0x18u) != 0 && (proposed & 0x18u) != 0x18u) NotImplemented_nid_no_patch("sceHttpsEnableOption: separate certificate date checks"); + node->settings.sslFlags = proposed; + return 0; +} + +int Header_nid_no_patch(int id, const char* name, const char* value, std::uint32_t mode) { + const auto node = find(id); + if (!node) return ERROR_INVALID_ID; + if (!name || !value || !token(name) || mode > 1 || std::string_view(value).find_first_of("\r\n") != std::string_view::npos) return ERROR_INVALID_VALUE; + if (equal(name, "Content-Length") || equal(name, "Transfer-Encoding")) NotImplemented_nid_no_patch("sceHttpAddRequestHeader: framing headers"); + std::lock_guard lock(node->mutex); + if (node->deleted) return ERROR_INVALID_ID; + if (node->sent) return ERROR_AFTER_SEND; + auto& headers = node->settings.headers; + if (mode == 0) std::erase_if(headers, [&](const auto& header) { return equal(header.first, name); }); + headers.emplace_back(name, value); + return 0; +} + +int Send_nid_no_patch(int id, const void* data, std::size_t size) { + const auto node = find(id); + if (!node || node->kind != Kind::Request) return ERROR_INVALID_ID; + if (size != 0 && !data) return ERROR_INVALID_VALUE; + std::unique_lock operation(node->operation, std::try_to_lock); + if (!operation.owns_lock()) return ERROR_HTTP_BUSY; + std::shared_ptr transport; + Settings settings; + { + std::lock_guard lock(node->mutex); + if (node->deleted) return ERROR_INVALID_ID; + if (node->aborted) return ERROR_HTTP_ABORTED; + if (node->sent) return ERROR_AFTER_SEND; + if (node->contentLength != size) return ERROR_INVALID_VALUE; + if (node->settings.redirect && std::any_of(node->settings.headers.begin(), node->settings.headers.end(), [](const auto& header) { + return equal(header.first, "Authorization") || equal(header.first, "Proxy-Authorization") || equal(header.first, "Cookie"); + })) NotImplemented_nid_no_patch("sceHttpSendRequest: automatic redirects with credential headers"); + transport = CreateTransport_nid_no_patch(); + node->transport = transport; + node->sent = true; + settings = node->settings; + } + const auto result = transport->Send(settings, node->url, node->method, data, size); + std::lock_guard lock(node->mutex); + node->result = node->aborted ? ERROR_HTTP_ABORTED : result; + node->ready = node->result == 0; + return node->result; +} + +int Read_nid_no_patch(int id, void* data, std::size_t size) { + const auto node = find(id); + if (!node || node->kind != Kind::Request) return ERROR_INVALID_ID; + if (size && !data) return ERROR_INVALID_VALUE; + std::unique_lock operation(node->operation, std::try_to_lock); + if (!operation.owns_lock()) return ERROR_HTTP_BUSY; + std::shared_ptr transport; + std::size_t blockSize = 0; + { + std::lock_guard lock(node->mutex); + if (node->deleted) return ERROR_INVALID_ID; + if (node->aborted) return ERROR_HTTP_ABORTED; + if (!node->ready) return node->result; + transport = node->transport; + blockSize = node->settings.receiveBlockSize; + } + if (size == 0) return 0; + const auto result = transport->Read(data, std::min(size, blockSize)); + std::lock_guard lock(node->mutex); + if (node->aborted) return ERROR_HTTP_ABORTED; + if (result < 0) { + node->ready = false; + node->result = result; + } + return result; +} + +int Status_nid_no_patch(int id, int* status) { + const auto node = find(id); + if (!node || node->kind != Kind::Request) return ERROR_INVALID_ID; + if (!status) return ERROR_INVALID_VALUE; + std::lock_guard lock(node->mutex); + if (!node->ready || node->aborted) return node->aborted ? ERROR_HTTP_ABORTED : node->result; + *status = node->transport->status; + return 0; +} + +int Headers_nid_no_patch(int id, char** headers, std::size_t* size) { + const auto node = find(id); + if (!node || node->kind != Kind::Request) return ERROR_INVALID_ID; + if (!headers || !size) return ERROR_INVALID_VALUE; + std::lock_guard lock(node->mutex); + if (!node->ready || node->aborted) return node->aborted ? ERROR_HTTP_ABORTED : node->result; + *headers = node->transport->headers.data(); + *size = node->transport->headers.size() + 1; + return 0; +} + +int Length_nid_no_patch(int id, int* result, std::uint64_t* length) { + const auto node = find(id); + if (!node || node->kind != Kind::Request) return ERROR_INVALID_ID; + if (!result || !length) return ERROR_INVALID_VALUE; + std::lock_guard lock(node->mutex); + if (!node->ready || node->aborted) return node->aborted ? ERROR_HTTP_ABORTED : node->result; + *result = node->transport->hasLength ? 0 : 1; + *length = node->transport->hasLength ? node->transport->length : 0; + return 0; +} + +int Abort_nid_no_patch(int id) { + const auto node = find(id); + if (!node || node->kind != Kind::Request) return ERROR_INVALID_ID; + std::shared_ptr transport; + { + std::lock_guard lock(node->mutex); + node->aborted = true; + transport = node->transport; + } + if (transport) transport->Abort(); + return 0; +} + +int Error_nid_no_patch(int id, int* error) { + const auto node = find(id); + if (!node || node->kind != Kind::Request) return ERROR_INVALID_ID; + if (!error) return ERROR_INVALID_VALUE; + std::lock_guard lock(node->mutex); + *error = node->transport ? node->transport->Error() : 0; + return 0; +} + +int SslError_nid_no_patch(int id, int* error, std::uint32_t* detail) { + if (!detail) return ERROR_INVALID_VALUE; + const auto result = Error_nid_no_patch(id, error); + if (result != 0) return result; + const auto node = find(id); + if (!node) return ERROR_INVALID_ID; + std::lock_guard lock(node->mutex); + *detail = node->transport ? node->transport->SslError() : 0; + return 0; +} + +int Unsupported_nid_no_patch(int id, bool requested, const char* function) { + if (!find(id)) return ERROR_INVALID_ID; + if (requested) NotImplemented_nid_no_patch(function); + return 0; +} + +#ifndef _WIN32 +std::shared_ptr CreateTransport_nid_no_patch() { + throw std::runtime_error("host HTTP is only implemented on Windows"); +} +#endif + +} diff --git a/core/libs/prx/libSceHttp/src/Network.hpp b/core/libs/prx/libSceHttp/src/Network.hpp new file mode 100644 index 00000000000..f43eb52b73b --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Network.hpp @@ -0,0 +1,39 @@ +#ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_NETWORK_HPP +#define CORE_LIBS_PRX_LIBSCEHTTP_SRC_NETWORK_HPP + +#include +#include + +namespace HttpNetwork { + +enum class Kind { Context, Template, Connection, Request }; +enum class Option { Redirect, ResolveTimeout, ConnectTimeout, SendTimeout, ReceiveTimeout, HeaderLimit, ContentLength, Gzip, ReceiveBlockSize }; + +extern "C" { + +bool Enabled_nid_no_patch(); +int Init_nid_no_patch(std::uint64_t poolSize); +int Template_nid_no_patch(int context, const char* agent, int version, int proxy); +int ConnectionUrl_nid_no_patch(int parent, const char* url, int keepAlive); +int ConnectionHost_nid_no_patch(int parent, const char* host, const char* scheme, std::uint16_t port, int keepAlive); +int RequestMethod_nid_no_patch(int parent, const char* method, const char* path, std::uint64_t length, bool absolute); +int RequestIndex_nid_no_patch(int parent, int method, const char* path, std::uint64_t length, bool absolute); +int Delete_nid_no_patch(int id, Kind kind); +int Configure_nid_no_patch(int id, Option option, std::uint64_t value); +int SslOptions_nid_no_patch(int id, std::uint32_t flags, bool enable); +int Header_nid_no_patch(int id, const char* name, const char* value, std::uint32_t mode); +int Send_nid_no_patch(int id, const void* data, std::size_t size); +int Read_nid_no_patch(int id, void* data, std::size_t size); +int Status_nid_no_patch(int id, int* status); +int Headers_nid_no_patch(int id, char** headers, std::size_t* size); +int Length_nid_no_patch(int id, int* result, std::uint64_t* length); +int Abort_nid_no_patch(int id); +int Error_nid_no_patch(int id, int* error); +int SslError_nid_no_patch(int id, int* error, std::uint32_t* detail); +int Unsupported_nid_no_patch(int id, bool requested, const char* function); + +} + +} + +#endif diff --git a/core/libs/prx/libSceHttp/src/Transport.hpp b/core/libs/prx/libSceHttp/src/Transport.hpp new file mode 100644 index 00000000000..a73d0cdeb67 --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Transport.hpp @@ -0,0 +1,47 @@ +#ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_TRANSPORT_HPP +#define CORE_LIBS_PRX_LIBSCEHTTP_SRC_TRANSPORT_HPP + +#include +#include +#include +#include +#include +#include + +namespace HttpNetwork { + +struct Settings { + std::string agent; + int version = 2; + bool redirect = true; + bool gzip = false; + bool keepAlive = true; + std::uint32_t resolveTimeout = 30000000; + std::uint32_t connectTimeout = 30000000; + std::uint32_t sendTimeout = 30000000; + std::uint32_t receiveTimeout = 30000000; + std::uint64_t headerLimit = 16384; + std::uint32_t sslFlags = 0x3d; + std::uint32_t receiveBlockSize = 16384; + std::vector> headers; +}; + +class ITransport { +public: + virtual ~ITransport() = default; + virtual int Send(const Settings& settings, const std::string& url, const std::string& method, const void* data, std::size_t size) = 0; + virtual int Read(void* data, std::size_t size) = 0; + virtual void Abort() = 0; + virtual int Error() const = 0; + virtual std::uint32_t SslError() const = 0; + std::string headers; + int status = 0; + bool hasLength = false; + std::uint64_t length = 0; +}; + +extern "C" std::shared_ptr CreateTransport_nid_no_patch(); + +} + +#endif From a7925b02e3a07e12dab07fa32f190d055d9a33d5 Mon Sep 17 00:00:00 2001 From: Aaditya Bhusal Date: Mon, 12 Oct 2026 08:47:03 +0300 Subject: [PATCH 2/4] feat(http): add cancellable Windows WinHTTP transport --- core/libs/prx/libSceHttp/CMakeLists.txt | 5 +- core/libs/prx/libSceHttp/src/WinHttp.cpp | 270 +++++++++++++++++++++++ 2 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceHttp/src/WinHttp.cpp diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 48a098bec61..409b289305c 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -1,5 +1,8 @@ -add_library(libSceHttp SHARED EXCLUDE_FROM_ALL Export.cpp src/Parse.cpp src/Uri.cpp) +add_library(libSceHttp SHARED EXCLUDE_FROM_ALL Export.cpp src/Parse.cpp src/Uri.cpp src/Network.cpp src/WinHttp.cpp) target_include_directories(libSceHttp PRIVATE ${LIBS_INCLUDE_DIR}) +if(WIN32) + target_link_libraries(libSceHttp PRIVATE winhttp) +endif() configure_windows_unwind(libSceHttp) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceHttp/src/WinHttp.cpp b/core/libs/prx/libSceHttp/src/WinHttp.cpp new file mode 100644 index 00000000000..5ddf1008844 --- /dev/null +++ b/core/libs/prx/libSceHttp/src/WinHttp.cpp @@ -0,0 +1,270 @@ +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#include "Transport.hpp" +#include "HttpErrors.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace HttpNetwork { +namespace { + +std::wstring wide(const std::string& value) { + const auto count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), nullptr, 0); + if (count == 0 && !value.empty()) return {}; + std::wstring result(count, L'\0'); + if (count) MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast(value.size()), result.data(), count); + return result; +} + +std::string narrow(std::wstring_view value) { + const auto count = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0, nullptr, nullptr); + std::string result(count, '\0'); + if (count) WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast(value.size()), result.data(), count, nullptr, nullptr); + return result; +} + +int milliseconds(std::uint32_t usec) { + return usec == 0 ? 0 : static_cast((static_cast(usec) + 999) / 1000); +} + +class WinHttpTransport final : public ITransport { +public: + ~WinHttpTransport() override { + close(); + if (connection) WinHttpCloseHandle(connection); + if (session) WinHttpCloseHandle(session); + } + + int Send(const Settings& settings, const std::string& url, const std::string& method, const void* data, std::size_t size) override { + receiveTimeout = settings.receiveTimeout; + if (size > std::numeric_limits::max()) return ERROR_INVALID_VALUE; + const auto encoded = wide(url); + if (encoded.empty()) return ERROR_INVALID_URL; + URL_COMPONENTS parts{}; + parts.dwStructSize = sizeof(parts); + parts.dwHostNameLength = static_cast(-1); + parts.dwUrlPathLength = static_cast(-1); + parts.dwExtraInfoLength = static_cast(-1); + if (!WinHttpCrackUrl(encoded.c_str(), static_cast(encoded.size()), 0, &parts)) return ERROR_INVALID_URL; + const auto agent = wide(settings.agent); + const auto verb = wide(method); + if ((!settings.agent.empty() && agent.empty()) || verb.empty()) return ERROR_INVALID_VALUE; + std::wstring host(parts.lpszHostName, parts.dwHostNameLength); + std::wstring path; + if (parts.dwUrlPathLength) path.assign(parts.lpszUrlPath, parts.dwUrlPathLength); + if (path.empty()) path = L"/"; + if (parts.dwExtraInfoLength) path.append(parts.lpszExtraInfo, parts.dwExtraInfoLength); + if (const auto fragment = path.find(L'#'); fragment != std::wstring::npos) path.resize(fragment); + { + std::lock_guard lock(apiMutex); + if (aborted) return ERROR_HTTP_ABORTED; + session = WinHttpOpen(agent.c_str(), WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC); + if (!session) return fail(GetLastError()); + if (!WinHttpSetTimeouts(session, milliseconds(settings.resolveTimeout), milliseconds(settings.connectTimeout), milliseconds(settings.sendTimeout), milliseconds(settings.receiveTimeout))) return fail(GetLastError()); + connection = WinHttpConnect(session, host.c_str(), parts.nPort, 0); + if (!connection) return fail(GetLastError()); + request = WinHttpOpenRequest(connection, verb.c_str(), path.c_str(), settings.version == 1 ? L"HTTP/1.0" : L"HTTP/1.1", WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); + if (!request) return fail(GetLastError()); + DWORD_PTR context = reinterpret_cast(this); + if (!WinHttpSetOption(request, WINHTTP_OPTION_CONTEXT_VALUE, &context, sizeof(context))) return fail(GetLastError()); + if (WinHttpSetStatusCallback(request, Callback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK) return fail(GetLastError()); + callbackInstalled = true; + DWORD disabled = WINHTTP_DISABLE_COOKIES | WINHTTP_DISABLE_AUTHENTICATION; + if (!settings.redirect) disabled |= WINHTTP_DISABLE_REDIRECTS; + if (!settings.keepAlive) disabled |= WINHTTP_DISABLE_KEEP_ALIVE; + if (!WinHttpSetOption(request, WINHTTP_OPTION_DISABLE_FEATURE, &disabled, sizeof(disabled))) return fail(GetLastError()); + DWORD redirectPolicy = settings.redirect ? WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP : WINHTTP_OPTION_REDIRECT_POLICY_NEVER; + if (!WinHttpSetOption(request, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy))) return fail(GetLastError()); + DWORD headerLimit = static_cast(settings.headerLimit); + if (!WinHttpSetOption(request, WINHTTP_OPTION_MAX_RESPONSE_HEADER_SIZE, &headerLimit, sizeof(headerLimit))) return fail(GetLastError()); + DWORD security = 0; + if (!(settings.sslFlags & 1u)) security = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE; + else { + if (!(settings.sslFlags & 4u)) security |= SECURITY_FLAG_IGNORE_CERT_CN_INVALID; + if (!(settings.sslFlags & 0x18u)) security |= SECURITY_FLAG_IGNORE_CERT_DATE_INVALID; + if (!(settings.sslFlags & 0x20u)) security |= SECURITY_FLAG_IGNORE_UNKNOWN_CA; + } + if (security && !WinHttpSetOption(request, WINHTTP_OPTION_SECURITY_FLAGS, &security, sizeof(security))) return fail(GetLastError()); + if (settings.gzip) { + DWORD decompression = WINHTTP_DECOMPRESSION_FLAG_GZIP; + if (!WinHttpSetOption(request, WINHTTP_OPTION_DECOMPRESSION, &decompression, sizeof(decompression))) return fail(GetLastError()); + } + for (const auto& [name, value] : settings.headers) { + const auto line = wide(name + ": " + value); + if (line.empty()) return ERROR_INVALID_VALUE; + if (!WinHttpAddRequestHeaders(request, line.c_str(), static_cast(line.size()), WINHTTP_ADDREQ_FLAG_ADD)) return fail(GetLastError()); + } + } + auto result = perform(WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE, [&] { + return WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, const_cast(data), static_cast(size), static_cast(size), reinterpret_cast(this)); + }); + if (result != 0) return result; + result = perform(WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE, [&] { return WinHttpReceiveResponse(request, nullptr); }, receiveTimeout); + if (result != 0) return result; + std::lock_guard lock(apiMutex); + if (aborted || !request) return ERROR_HTTP_ABORTED; + DWORD code = 0; + DWORD bytes = sizeof(code); + if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &code, &bytes, WINHTTP_NO_HEADER_INDEX)) return fail(GetLastError()); + status = static_cast(code); + bytes = 0; + if (WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, nullptr, &bytes, WINHTTP_NO_HEADER_INDEX) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) return fail(GetLastError()); + if (bytes > (settings.headerLimit + 1) * sizeof(wchar_t)) return ERROR_HTTP_HEADER_TOO_LARGE; + std::wstring raw((bytes + sizeof(wchar_t) - 1) / sizeof(wchar_t), L'\0'); + if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, raw.data(), &bytes, WINHTTP_NO_HEADER_INDEX)) return fail(GetLastError()); + raw.resize(bytes / sizeof(wchar_t)); + while (!raw.empty() && raw.back() == L'\0') raw.pop_back(); + headers = narrow(raw); + if (headers.size() > settings.headerLimit) return ERROR_HTTP_HEADER_TOO_LARGE; + wchar_t lengthBuffer[32]{}; + bytes = sizeof(lengthBuffer); + if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_LENGTH, WINHTTP_HEADER_NAME_BY_INDEX, lengthBuffer, &bytes, WINHTTP_NO_HEADER_INDEX)) { + const auto value = narrow(lengthBuffer); + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), length); + hasLength = parsed.ec == std::errc{} && parsed.ptr == value.data() + value.size(); + } else if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND) return fail(GetLastError()); + return 0; + } + + int Read(void* data, std::size_t size) override { + const auto result = perform(WINHTTP_CALLBACK_STATUS_READ_COMPLETE, [&] { return WinHttpReadData(request, data, static_cast(size), nullptr); }, receiveTimeout); + return result == 0 ? static_cast(readBytes) : result; + } + + void Abort() override { + aborted = true; + close(); + } + + int Error() const override { return static_cast(lastError.load()); } + std::uint32_t SslError() const override { return sslError.load(); } + +private: + HINTERNET session = nullptr; + HINTERNET connection = nullptr; + HINTERNET request = nullptr; + std::mutex apiMutex; + std::mutex completionMutex; + std::condition_variable completionChanged; + std::atomic aborted = false; + std::atomic lastError = 0; + std::atomic sslError = 0; + bool callbackInstalled = false; + bool closed = false; + bool completed = false; + DWORD expected = 0; + DWORD operationError = 0; + DWORD readBytes = 0; + std::uint32_t receiveTimeout = 0; + std::chrono::steady_clock::time_point deadline = std::chrono::steady_clock::time_point::max(); + + int fail(DWORD error) { + lastError = error; + if (aborted || error == ERROR_WINHTTP_OPERATION_CANCELLED) return ERROR_HTTP_ABORTED; + if (error == ERROR_WINHTTP_TIMEOUT) return ERROR_HTTP_TIMEOUT; + if (error == ERROR_WINHTTP_SECURE_FAILURE) return ERROR_HTTP_SSL; + if (error == ERROR_WINHTTP_HEADER_SIZE_OVERFLOW) return ERROR_HTTP_HEADER_TOO_LARGE; + if (error == ERROR_NOT_ENOUGH_MEMORY || error == ERROR_OUTOFMEMORY) return ERROR_OUT_OF_MEMORY; + return ERROR_NETWORK; + } + + template + int perform(DWORD notification, TCall call, std::uint32_t timeout = 0) { + DWORD immediateError = 0; + { + std::lock_guard lock(apiMutex); + if (aborted || !request) return ERROR_HTTP_ABORTED; + { + std::lock_guard completionLock(completionMutex); + completed = false; + expected = notification; + operationError = 0; + deadline = timeout ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) : std::chrono::steady_clock::time_point::max(); + } + if (!call()) immediateError = GetLastError(); + } + if (immediateError) { + close(); + return fail(immediateError); + } + std::unique_lock lock(completionMutex); + while (!completed && !closed) { + if (deadline == std::chrono::steady_clock::time_point::max()) completionChanged.wait(lock); + else if (completionChanged.wait_until(lock, deadline) == std::cv_status::timeout && !completed && !closed) { + lock.unlock(); + close(); + return fail(ERROR_WINHTTP_TIMEOUT); + } + } + const auto error = operationError; + lock.unlock(); + if (aborted) { + close(); + return ERROR_HTTP_ABORTED; + } + if (error) { + close(); + return fail(error); + } + return 0; + } + + void close() { + bool waitForClose = false; + { + std::lock_guard lock(apiMutex); + waitForClose = callbackInstalled; + if (request) { + const auto handle = request; + if (!WinHttpCloseHandle(handle)) std::terminate(); + request = nullptr; + } + } + if (waitForClose) { + std::unique_lock lock(completionMutex); + completionChanged.wait(lock, [&] { return closed; }); + } + } + + static void CALLBACK Callback(HINTERNET, DWORD_PTR context, DWORD status, void* information, DWORD size) { + if (!context) return; + auto& self = *reinterpret_cast(context); + std::lock_guard lock(self.completionMutex); + if (status == WINHTTP_CALLBACK_STATUS_REQUEST_SENT && self.expected == WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE && self.receiveTimeout) { + self.deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(self.receiveTimeout); + self.completionChanged.notify_all(); + } else if (status == WINHTTP_CALLBACK_STATUS_SECURE_FAILURE) { + if (information && size == sizeof(DWORD)) self.sslError = *static_cast(information); + } else if (status == WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING) { + self.closed = true; + self.operationError = ERROR_WINHTTP_OPERATION_CANCELLED; + self.completionChanged.notify_all(); + } else if (status == WINHTTP_CALLBACK_STATUS_REQUEST_ERROR) { + self.operationError = static_cast(information)->dwError; + self.completed = true; + self.completionChanged.notify_all(); + } else if (status == self.expected) { + self.readBytes = status == WINHTTP_CALLBACK_STATUS_READ_COMPLETE ? size : 0; + self.completed = true; + self.completionChanged.notify_all(); + } + } +}; + +} + +std::shared_ptr CreateTransport_nid_no_patch() { + return std::make_shared(); +} + +} +#endif From 941c5e4a83b99655518676ee7960e71acef6b790 Mon Sep 17 00:00:00 2001 From: Aaditya Bhusal Date: Mon, 12 Oct 2026 08:47:03 +0300 Subject: [PATCH 3/4] refactor(http): dispatch exports to offline and network backends --- core/libs/prx/libSceHttp/CMakeLists.txt | 2 +- core/libs/prx/libSceHttp/Export.cpp | 538 ++++++++--------------- core/libs/prx/libSceHttp/src/Offline.cpp | 533 ++++++++++++++++++++++ core/libs/prx/libSceHttp/src/Offline.hpp | 80 ++++ 4 files changed, 794 insertions(+), 359 deletions(-) create mode 100644 core/libs/prx/libSceHttp/src/Offline.cpp create mode 100644 core/libs/prx/libSceHttp/src/Offline.hpp diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 409b289305c..86009295520 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceHttp SHARED EXCLUDE_FROM_ALL Export.cpp src/Parse.cpp src/Uri.cpp src/Network.cpp src/WinHttp.cpp) +add_library(libSceHttp SHARED EXCLUDE_FROM_ALL Export.cpp src/Parse.cpp src/Uri.cpp src/Network.cpp src/Offline.cpp src/WinHttp.cpp) target_include_directories(libSceHttp PRIVATE ${LIBS_INCLUDE_DIR}) if(WIN32) target_link_libraries(libSceHttp PRIVATE winhttp) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 86f20a8700d..efa616f5538 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -1,439 +1,286 @@ -#include #include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#endif -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" -#include "prx/libSceHttp/src/HttpErrors.hpp" -#include -#include -#include +#include #include -#include -#include -#include #include #include -#include -#include -#include - -// No network is emulated: contexts, templates and requests can be created, but any request -// that would touch the network fails with the library's network error. -static std::atomic g_nextHandle{1}; -using HttpRequestStatusCallback = void (APS5_VABI *)(int, int, void*); -struct RequestStatusRegistration { - HttpRequestStatusCallback callback; - void* argument; -}; -static std::mutex g_statusMutex; -static std::map g_statusCallbacks; -struct HttpsCaList { - void* certs; - int certsNum; -}; - -#ifdef _WIN32 -struct HostMutex { - SRWLOCK native = SRWLOCK_INIT; - - HostMutex() = default; - HostMutex(const HostMutex&) = delete; - HostMutex& operator=(const HostMutex&) = delete; - - void lock() { AcquireSRWLockExclusive(&native); } - void unlock() { ReleaseSRWLockExclusive(&native); } -}; -#else -using HostMutex = std::mutex; -#endif - -class HostCondition { -public: - HostCondition() = default; - HostCondition(const HostCondition&) = delete; - HostCondition& operator=(const HostCondition&) = delete; - - void NotifyAll() { -#ifdef _WIN32 - WakeAllConditionVariable(&native); -#else - native.notify_all(); -#endif - } - - void Wait(std::unique_lock& lock) { -#ifdef _WIN32 - if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, INFINITE, 0)) - throw std::runtime_error("libSceHttp: native condition wait failed"); -#else - native.wait(lock); -#endif - } - - void WaitUntil(std::unique_lock& lock, std::chrono::steady_clock::time_point deadline) { -#ifdef _WIN32 - const auto now = std::chrono::steady_clock::now(); - if (deadline <= now) return; - const auto milliseconds = std::chrono::ceil(deadline - now).count(); - if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, static_cast(milliseconds), 0) && - GetLastError() != ERROR_TIMEOUT) - throw std::runtime_error("libSceHttp: native condition wait failed"); -#else - native.wait_until(lock, deadline); -#endif - } - -private: -#ifdef _WIN32 - CONDITION_VARIABLE native = CONDITION_VARIABLE_INIT; -#else - std::condition_variable native; -#endif -}; +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libSceHttp/src/HttpErrors.hpp" +#include "prx/libSceHttp/src/Network.hpp" +#include "prx/libSceHttp/src/Offline.hpp" -struct HttpEpoll { - HostMutex mutex; - HostCondition wake; - bool aborted = false; - bool destroyed = false; -}; +namespace { -static std::mutex g_stateMutex; -static std::unordered_set g_contexts; -static std::unordered_map> g_epolls; +int UnsupportedEpoll_nid_no_patch(const char* function) { + NotImplemented_nid_no_patch(function); + return 0; +} -static std::shared_ptr FindEpoll(HttpEpollHandle eh) { - const auto it = g_epolls.find(eh); - return it == g_epolls.end() ? nullptr : it->second; +int CookieEnabled_nid_no_patch(int id, int enable) { + if (static_cast(enable) > 1) return ERROR_INVALID_VALUE; + return HttpNetwork::Unsupported_nid_no_patch(id, enable != 0, "sceHttpSetCookieEnabled"); } -static void MarkDestroyed(HttpEpoll& epoll) { - std::lock_guard lock(epoll.mutex); - epoll.destroyed = true; - epoll.wake.NotifyAll(); } extern "C" { int APS5_VABI sceHttpAbortRequest(int request_id) { - (void)request_id; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Abort_nid_no_patch(request_id) + : HttpOffline::OfflineHttpAbortRequest_nid_no_patch(request_id); } int APS5_VABI sceHttpAddRequestHeader(int id, const char* name, const char* value, uint32_t mode) { - (void)id; - (void)name; - (void)value; - (void)mode; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Header_nid_no_patch(id, name, value, mode) + : HttpOffline::OfflineHttpAddRequestHeader_nid_no_patch(id, name, value, mode); } int APS5_VABI sceHttpCreateConnection(int tmpl_id, const char* server_name, const char* scheme, uint16_t port, int enable_keep_alive) { - (void)tmpl_id; - (void)server_name; - (void)scheme; - (void)port; - (void)enable_keep_alive; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::ConnectionHost_nid_no_patch(tmpl_id, server_name, scheme, port, enable_keep_alive) + : HttpOffline::OfflineHttpCreateConnection_nid_no_patch(tmpl_id, server_name, scheme, port, enable_keep_alive); } int APS5_VABI sceHttpCreateConnectionWithURL(int tmpl_id, const char* url, int enable_keep_alive) { - (void)tmpl_id; - (void)url; - (void)enable_keep_alive; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::ConnectionUrl_nid_no_patch(tmpl_id, url, enable_keep_alive) + : HttpOffline::OfflineHttpCreateConnectionWithURL_nid_no_patch(tmpl_id, url, enable_keep_alive); } int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { - std::lock_guard lock(g_stateMutex); - if (g_contexts.empty()) return ERROR_BEFORE_INIT; - if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; - if (!eh) return ERROR_INVALID_VALUE; - auto epoll = std::make_shared(); - *eh = epoll.get(); - g_epolls.emplace(epoll.get(), std::move(epoll)); - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(http_ctx_id, true, __func__) + : HttpOffline::OfflineHttpCreateEpoll_nid_no_patch(http_ctx_id, eh); } int APS5_VABI sceHttpCreateRequest(int conn_id, int method, const char* path, uint64_t content_length) { - (void)conn_id; - (void)method; - (void)path; - (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::RequestIndex_nid_no_patch(conn_id, method, path, content_length, false) + : HttpOffline::OfflineHttpCreateRequest_nid_no_patch(conn_id, method, path, content_length); } int APS5_VABI sceHttpCreateRequestWithURL2(int conn_id, const char* method, const char* url, uint64_t content_length) { - (void)conn_id; - (void)method; - (void)url; - (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::RequestMethod_nid_no_patch(conn_id, method, url, content_length, true) + : HttpOffline::OfflineHttpCreateRequestWithURL2_nid_no_patch(conn_id, method, url, content_length); } int APS5_VABI sceHttpCreateTemplate(int http_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf) { - (void)http_ctx_id; - (void)user_agent; - (void)http_ver; - (void)is_auto_proxy_conf; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Template_nid_no_patch(http_ctx_id, user_agent, http_ver, is_auto_proxy_conf) + : HttpOffline::OfflineHttpCreateTemplate_nid_no_patch(http_ctx_id, user_agent, http_ver, is_auto_proxy_conf); } int APS5_VABI sceHttpDeleteConnection(int conn_id) { - (void)conn_id; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Delete_nid_no_patch(conn_id, HttpNetwork::Kind::Connection) + : HttpOffline::OfflineHttpDeleteConnection_nid_no_patch(conn_id); } int APS5_VABI sceHttpDeleteRequest(int req_id) { - std::lock_guard lock(g_statusMutex); - g_statusCallbacks.erase(req_id); - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Delete_nid_no_patch(req_id, HttpNetwork::Kind::Request) + : HttpOffline::OfflineHttpDeleteRequest_nid_no_patch(req_id); } int APS5_VABI sceHttpDeleteTemplate(int tmpl_id) { - (void)tmpl_id; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Delete_nid_no_patch(tmpl_id, HttpNetwork::Kind::Template) + : HttpOffline::OfflineHttpDeleteTemplate_nid_no_patch(tmpl_id); } int APS5_VABI sceHttpDestroyEpoll(int http_ctx_id, HttpEpollHandle eh) { - std::shared_ptr epoll; - { - std::lock_guard lock(g_stateMutex); - if (g_contexts.empty()) return ERROR_BEFORE_INIT; - if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; - if (!eh) return ERROR_INVALID_VALUE; - epoll = FindEpoll(eh); - if (!epoll) return ERROR_INVALID_ID; - g_epolls.erase(eh); - } - MarkDestroyed(*epoll); - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(http_ctx_id, true, __func__) + : HttpOffline::OfflineHttpDestroyEpoll_nid_no_patch(http_ctx_id, eh); } int APS5_VABI sceHttpGetAllResponseHeaders(int request_id, char** header, size_t* header_size) { - (void)request_id; - (void)header; - (void)header_size; - return ERROR_NETWORK; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Headers_nid_no_patch(request_id, header, header_size) + : HttpOffline::OfflineHttpGetAllResponseHeaders_nid_no_patch(request_id, header, header_size); } int APS5_VABI sceHttpGetResponseContentLength(int request_id, int* result, uint64_t* content_length) { - (void)request_id; - (void)result; - (void)content_length; - return ERROR_NETWORK; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Length_nid_no_patch(request_id, result, content_length) + : HttpOffline::OfflineHttpGetResponseContentLength_nid_no_patch(request_id, result, content_length); } int APS5_VABI sceHttpGetStatusCode(int request_id, int* status_code) { - (void)request_id; - (void)status_code; - return ERROR_NETWORK; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Status_nid_no_patch(request_id, status_code) + : HttpOffline::OfflineHttpGetStatusCode_nid_no_patch(request_id, status_code); } int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_size) { - (void)memid; - (void)ssl_ctx_id; - (void)pool_size; - const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_stateMutex); - g_contexts.insert(id); - return id; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Init_nid_no_patch(pool_size) + : HttpOffline::OfflineHttpInit_nid_no_patch(memid, ssl_ctx_id, pool_size); } int APS5_VABI sceHttpsDisableOption(int id, uint32_t ssl_flags) { - (void)id; - (void)ssl_flags; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::SslOptions_nid_no_patch(id, ssl_flags, false) + : HttpOffline::OfflineHttpsDisableOption_nid_no_patch(id, ssl_flags); } int APS5_VABI sceHttpSendRequest(int request_id, const void* post_data, size_t size) { - (void)request_id; - (void)post_data; - (void)size; - return ERROR_NETWORK; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Send_nid_no_patch(request_id, post_data, size) + : HttpOffline::OfflineHttpSendRequest_nid_no_patch(request_id, post_data, size); } int APS5_VABI sceHttpSetAuthEnabled(int id, int enable) { - (void)id; - (void)enable; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, enable != 0, __func__) + : HttpOffline::OfflineHttpSetAuthEnabled_nid_no_patch(id, enable); } int APS5_VABI sceHttpSetAuthInfoCallback(int id, HttpAuthInfoCallback callback, void* userArg) { - (void)id; - (void)callback; - (void)userArg; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, callback != nullptr, __func__) + : HttpOffline::OfflineHttpSetAuthInfoCallback_nid_no_patch(id, callback, userArg); } int APS5_VABI sceHttpSetCookieEnabled(int id, int enable) { - (void)id; - if (static_cast(enable) > 1) return ERROR_INVALID_VALUE; - if (enable != 0) NotImplemented_nid_no_patch(__func__); - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? CookieEnabled_nid_no_patch(id, enable) + : HttpOffline::OfflineHttpSetCookieEnabled_nid_no_patch(id, enable); } int APS5_VABI sceHttpSetAutoRedirect(int id, int enable) { - (void)id; - (void)enable; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::Redirect, static_cast(enable)) + : HttpOffline::OfflineHttpSetAutoRedirect_nid_no_patch(id, enable); } int APS5_VABI sceHttpSetConnectTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::ConnectTimeout, usec) + : HttpOffline::OfflineHttpSetConnectTimeOut_nid_no_patch(id, usec); } int APS5_VABI sceHttpSetEpoll(int id, HttpEpollHandle eh, void* user_arg) { - (void)id; - (void)eh; - (void)user_arg; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, true, __func__) + : HttpOffline::OfflineHttpSetEpoll_nid_no_patch(id, eh, user_arg); } int APS5_VABI sceHttpSetNonblock(int id, int enable) { - (void)id; - (void)enable; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, enable != 0, __func__) + : HttpOffline::OfflineHttpSetNonblock_nid_no_patch(id, enable); } int APS5_VABI sceHttpSetRecvTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::ReceiveTimeout, usec) + : HttpOffline::OfflineHttpSetRecvTimeOut_nid_no_patch(id, usec); } int APS5_VABI sceHttpSetRecvBlockSize(int id, uint32_t block_size) { - (void)id; - (void)block_size; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::ReceiveBlockSize, block_size) + : HttpOffline::OfflineHttpSetRecvBlockSize_nid_no_patch(id, block_size); } int APS5_VABI sceHttpSetResponseHeaderMaxSize(int id, uint64_t header_size) { - (void)id; - (void)header_size; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::HeaderLimit, header_size) + : HttpOffline::OfflineHttpSetResponseHeaderMaxSize_nid_no_patch(id, header_size); } int APS5_VABI sceHttpSetRequestContentLength(int request_id, uint64_t content_length) { - (void)request_id; - (void)content_length; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(request_id, HttpNetwork::Option::ContentLength, content_length) + : HttpOffline::OfflineHttpSetRequestContentLength_nid_no_patch(request_id, content_length); } int APS5_VABI sceHttpSetResolveRetry(int id, int32_t retry) { - (void)id; - (void)retry; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, retry != 0, __func__) + : HttpOffline::OfflineHttpSetResolveRetry_nid_no_patch(id, retry); } int APS5_VABI sceHttpSetResolveTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::ResolveTimeout, usec) + : HttpOffline::OfflineHttpSetResolveTimeOut_nid_no_patch(id, usec); } int APS5_VABI sceHttpSetSendTimeOut(int id, uint32_t usec) { - (void)id; - (void)usec; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::SendTimeout, usec) + : HttpOffline::OfflineHttpSetSendTimeOut_nid_no_patch(id, usec); } int APS5_VABI sceHttpsSetMinSslVersion(int id, uint32_t ssl_version) { - (void)id; - (void)ssl_version; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, ssl_version != 0, __func__) + : HttpOffline::OfflineHttpsSetMinSslVersion_nid_no_patch(id, ssl_version); } int APS5_VABI sceHttpsSetSslCallback(int id, HttpsCallback cbfunc, void* user_arg) { - (void)id; - (void)cbfunc; - (void)user_arg; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, cbfunc != nullptr, __func__) + : HttpOffline::OfflineHttpsSetSslCallback_nid_no_patch(id, cbfunc, user_arg); } int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { - std::vector> epolls; - { - std::lock_guard lock(g_stateMutex); - if (g_contexts.empty()) return ERROR_BEFORE_INIT; - if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; - if (!g_contexts.empty()) return 0; - for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); - g_epolls.clear(); - } - for (const auto& epoll : epolls) MarkDestroyed(*epoll); - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Delete_nid_no_patch(http_ctx_id, HttpNetwork::Kind::Context) + : HttpOffline::OfflineHttpTerm_nid_no_patch(http_ctx_id); } int APS5_VABI sceHttpUnsetEpoll(int id) { - (void)id; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, true, __func__) + : HttpOffline::OfflineHttpUnsetEpoll_nid_no_patch(id); } int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { - std::shared_ptr epoll; - { - std::lock_guard lock(g_stateMutex); - if (g_contexts.empty()) return ERROR_BEFORE_INIT; - if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; - epoll = FindEpoll(eh); - if (!epoll) return ERROR_INVALID_ID; - } - std::unique_lock lock(epoll->mutex); - const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; - if (timeout < 0) { - while (!woken()) epoll->wake.Wait(lock); - } else { - const auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(timeout); - while (!woken() && std::chrono::steady_clock::now() < deadline) epoll->wake.WaitUntil(lock, deadline); - } - return epoll->aborted ? ERROR_ABORTED : 0; + return HttpNetwork::Enabled_nid_no_patch() + ? UnsupportedEpoll_nid_no_patch(__func__) + : HttpOffline::OfflineHttpWaitRequest_nid_no_patch(eh, nbev, maxevents, timeout); } int APS5_VABI sceHttpCreateRequestWithURL(int conn_id, int method, const char* url, uint64_t content_length) { - (void)conn_id; - (void)method; - (void)url; - (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::RequestIndex_nid_no_patch(conn_id, method, url, content_length, true) + : HttpOffline::OfflineHttpCreateRequestWithURL_nid_no_patch(conn_id, method, url, content_length); } int APS5_VABI sceHttpReadData(int request_id, void* data, size_t size) { - (void)request_id; - (void)data; - (void)size; - return ERROR_NETWORK; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Read_nid_no_patch(request_id, data, size) + : HttpOffline::OfflineHttpReadData_nid_no_patch(request_id, data, size); } int APS5_VABI sceHttpRedirectCacheFlush(int http_ctx_id) { - (void)http_ctx_id; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(http_ctx_id, true, __func__) + : HttpOffline::OfflineHttpRedirectCacheFlush_nid_no_patch(http_ctx_id); } int APS5_VABI sceHttpSetChunkedTransferEnabled(int id, int enable) { - (void)id; - (void)enable; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, enable != 0, __func__) + : HttpOffline::OfflineHttpSetChunkedTransferEnabled_nid_no_patch(id, enable); } - int APS5_VABI sceHttpSetInflateGZIPEnabled(int id, int enable) { - (void)id; - if (static_cast(enable) > 1) return ERROR_INVALID_VALUE; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Configure_nid_no_patch(id, HttpNetwork::Option::Gzip, static_cast(enable)) + : HttpOffline::OfflineHttpSetInflateGZIPEnabled_nid_no_patch(id, enable); } int APS5_VABI sceHttpSetRequestStatusCallback(int id, HttpRequestStatusCallback callback, void* argument) { - if (id <= 0 || id >= g_nextHandle.load(std::memory_order_relaxed)) return static_cast(0x80431100u); - std::lock_guard lock(g_statusMutex); - if (callback) g_statusCallbacks[id] = {callback, argument}; - else g_statusCallbacks.erase(id); - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, callback != nullptr, __func__) + : HttpOffline::OfflineHttpSetRequestStatusCallback_nid_no_patch(id, callback, argument); } int APS5_VABI sceHttpParseResponseHeader(const char* header, std::size_t headerLen, const char* fieldStr, @@ -499,100 +346,75 @@ int APS5_VABI sceHttpParseResponseHeader(const char* header, std::size_t headerL } int APS5_VABI sceHttpCreateRequest2(int conn_id, const char* method, const char* path, uint64_t content_length) { - (void)conn_id; - (void)method; - (void)path; - (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::RequestMethod_nid_no_patch(conn_id, method, path, content_length, false) + : HttpOffline::OfflineHttpCreateRequest2_nid_no_patch(conn_id, method, path, content_length); } int APS5_VABI sceHttpsEnableOption(int id, uint32_t ssl_flags) { - (void)id; - (void)ssl_flags; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::SslOptions_nid_no_patch(id, ssl_flags, true) + : HttpOffline::OfflineHttpsEnableOption_nid_no_patch(id, ssl_flags); } int APS5_VABI sceHttpsLoadCert(int http_ctx_id, int num, void* ca_list, void* cert, void* key) { - (void)http_ctx_id; - (void)num; - (void)ca_list; - (void)cert; - (void)key; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(http_ctx_id, true, __func__) + : HttpOffline::OfflineHttpsLoadCert_nid_no_patch(http_ctx_id, num, ca_list, cert, key); } int APS5_VABI sceHttpsUnloadCert(int http_ctx_id) { - (void)http_ctx_id; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(http_ctx_id, true, __func__) + : HttpOffline::OfflineHttpsUnloadCert_nid_no_patch(http_ctx_id); } int APS5_VABI sceHttpGetLastErrno(int request_id, int* errno_out) { - (void)request_id; - if (errno_out == nullptr) { - return ERROR_INVALID_VALUE; - } - *errno_out = 0; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Error_nid_no_patch(request_id, errno_out) + : HttpOffline::OfflineHttpGetLastErrno_nid_no_patch(request_id, errno_out); } int APS5_VABI sceHttpsGetSslError(int id, int* err_num, uint32_t* detail) { - (void)id; - if (err_num == nullptr || detail == nullptr) { - return ERROR_INVALID_VALUE; - } - *err_num = 0; - *detail = 0; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::SslError_nid_no_patch(id, err_num, detail) + : HttpOffline::OfflineHttpsGetSslError_nid_no_patch(id, err_num, detail); } int APS5_VABI sceHttpSetCookieRecvCallback(int id, HttpCookieRecvCallback cbfunc, void* user_arg) { - (void)id; - (void)cbfunc; - (void)user_arg; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, cbfunc != nullptr, __func__) + : HttpOffline::OfflineHttpSetCookieRecvCallback_nid_no_patch(id, cbfunc, user_arg); } int APS5_VABI sceHttpsSetSslVersion(int id, int ssl_version) { - (void)id; - (void)ssl_version; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, ssl_version != 0, __func__) + : HttpOffline::OfflineHttpsSetSslVersion_nid_no_patch(id, ssl_version); } int APS5_VABI sceHttpSetRedirectCallback(int id, HttpRedirectCallback cbfunc, void* user_arg) { - (void)id; - (void)cbfunc; - (void)user_arg; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(id, cbfunc != nullptr, __func__) + : HttpOffline::OfflineHttpSetRedirectCallback_nid_no_patch(id, cbfunc, user_arg); } int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle eh) { - std::shared_ptr epoll; - { - std::lock_guard lock(g_stateMutex); - if (g_contexts.empty()) return ERROR_BEFORE_INIT; - if (!eh) return ERROR_INVALID_VALUE; - epoll = FindEpoll(eh); - if (!epoll) return ERROR_INVALID_ID; - } - std::lock_guard lock(epoll->mutex); - epoll->aborted = true; - epoll->wake.NotifyAll(); - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? UnsupportedEpoll_nid_no_patch(__func__) + : HttpOffline::OfflineHttpAbortWaitRequest_nid_no_patch(eh); } int APS5_VABI sceHttpsGetCaList(int http_ctx_id, HttpsCaList* ca_list) { - (void)http_ctx_id; - if (!ca_list) return ERROR_INVALID_VALUE; - ca_list->certs = nullptr; - ca_list->certsNum = 0; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(http_ctx_id, true, __func__) + : HttpOffline::OfflineHttpsGetCaList_nid_no_patch(http_ctx_id, ca_list); } int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { - (void)http_ctx_id; - if (!ca_list) return ERROR_INVALID_VALUE; - ca_list->certs = nullptr; - ca_list->certsNum = 0; - return 0; + return HttpNetwork::Enabled_nid_no_patch() + ? HttpNetwork::Unsupported_nid_no_patch(http_ctx_id, true, __func__) + : HttpOffline::OfflineHttpsFreeCaList_nid_no_patch(http_ctx_id, ca_list); } + } diff --git a/core/libs/prx/libSceHttp/src/Offline.cpp b/core/libs/prx/libSceHttp/src/Offline.cpp new file mode 100644 index 00000000000..10e30f327d7 --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Offline.cpp @@ -0,0 +1,533 @@ +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif +#include "Offline.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libSceHttp/src/HttpErrors.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// No network is emulated: contexts, templates and requests can be created, but any request +// that would touch the network fails with the library's network error. +static std::atomic g_nextHandle{1}; +struct RequestStatusRegistration { + HttpRequestStatusCallback callback; + void* argument; +}; +static std::mutex g_statusMutex; +static std::map g_statusCallbacks; + +#ifdef _WIN32 +struct HostMutex { + SRWLOCK native = SRWLOCK_INIT; + + HostMutex() = default; + HostMutex(const HostMutex&) = delete; + HostMutex& operator=(const HostMutex&) = delete; + + void lock() { AcquireSRWLockExclusive(&native); } + void unlock() { ReleaseSRWLockExclusive(&native); } +}; +#else +using HostMutex = std::mutex; +#endif + +class HostCondition { +public: + HostCondition() = default; + HostCondition(const HostCondition&) = delete; + HostCondition& operator=(const HostCondition&) = delete; + + void NotifyAll() { +#ifdef _WIN32 + WakeAllConditionVariable(&native); +#else + native.notify_all(); +#endif + } + + void Wait(std::unique_lock& lock) { +#ifdef _WIN32 + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, INFINITE, 0)) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait(lock); +#endif + } + + void WaitUntil(std::unique_lock& lock, std::chrono::steady_clock::time_point deadline) { +#ifdef _WIN32 + const auto now = std::chrono::steady_clock::now(); + if (deadline <= now) return; + const auto milliseconds = std::chrono::ceil(deadline - now).count(); + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, static_cast(milliseconds), 0) && + GetLastError() != ERROR_TIMEOUT) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait_until(lock, deadline); +#endif + } + +private: +#ifdef _WIN32 + CONDITION_VARIABLE native = CONDITION_VARIABLE_INIT; +#else + std::condition_variable native; +#endif +}; + +struct HttpEpoll { + HostMutex mutex; + HostCondition wake; + bool aborted = false; + bool destroyed = false; +}; + +static std::mutex g_stateMutex; +static std::unordered_set g_contexts; +static std::unordered_map> g_epolls; + +static std::shared_ptr FindEpoll_nid_no_patch(HttpEpollHandle eh) { + const auto it = g_epolls.find(eh); + return it == g_epolls.end() ? nullptr : it->second; +} + +static void MarkDestroyed_nid_no_patch(HttpEpoll& epoll) { + std::lock_guard lock(epoll.mutex); + epoll.destroyed = true; + epoll.wake.NotifyAll(); +} + +namespace HttpOffline { + +extern "C" { + +int OfflineHttpAbortRequest_nid_no_patch(int request_id) { + (void)request_id; + return 0; +} + +int OfflineHttpAddRequestHeader_nid_no_patch(int id, const char* name, const char* value, uint32_t mode) { + (void)id; + (void)name; + (void)value; + (void)mode; + return 0; +} + +int OfflineHttpCreateConnection_nid_no_patch(int tmpl_id, const char* server_name, const char* scheme, uint16_t port, int enable_keep_alive) { + (void)tmpl_id; + (void)server_name; + (void)scheme; + (void)port; + (void)enable_keep_alive; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int OfflineHttpCreateConnectionWithURL_nid_no_patch(int tmpl_id, const char* url, int enable_keep_alive) { + (void)tmpl_id; + (void)url; + (void)enable_keep_alive; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int OfflineHttpCreateEpoll_nid_no_patch(int http_ctx_id, HttpEpollHandle* eh) { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; + if (!eh) return ERROR_INVALID_VALUE; + auto epoll = std::make_shared(); + *eh = epoll.get(); + g_epolls.emplace(epoll.get(), std::move(epoll)); + return 0; +} + +int OfflineHttpCreateRequest_nid_no_patch(int conn_id, int method, const char* path, uint64_t content_length) { + (void)conn_id; + (void)method; + (void)path; + (void)content_length; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int OfflineHttpCreateRequestWithURL2_nid_no_patch(int conn_id, const char* method, const char* url, uint64_t content_length) { + (void)conn_id; + (void)method; + (void)url; + (void)content_length; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int OfflineHttpCreateTemplate_nid_no_patch(int http_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf) { + (void)http_ctx_id; + (void)user_agent; + (void)http_ver; + (void)is_auto_proxy_conf; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int OfflineHttpDeleteConnection_nid_no_patch(int conn_id) { + (void)conn_id; + return 0; +} + +int OfflineHttpDeleteRequest_nid_no_patch(int req_id) { + std::lock_guard lock(g_statusMutex); + g_statusCallbacks.erase(req_id); + return 0; +} + +int OfflineHttpDeleteTemplate_nid_no_patch(int tmpl_id) { + (void)tmpl_id; + return 0; +} + +int OfflineHttpDestroyEpoll_nid_no_patch(int http_ctx_id, HttpEpollHandle eh) { + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll_nid_no_patch(eh); + if (!epoll) return ERROR_INVALID_ID; + g_epolls.erase(eh); + } + MarkDestroyed_nid_no_patch(*epoll); + return 0; +} + +int OfflineHttpGetAllResponseHeaders_nid_no_patch(int request_id, char** header, size_t* header_size) { + (void)request_id; + (void)header; + (void)header_size; + return ERROR_NETWORK; +} + +int OfflineHttpGetResponseContentLength_nid_no_patch(int request_id, int* result, uint64_t* content_length) { + (void)request_id; + (void)result; + (void)content_length; + return ERROR_NETWORK; +} + +int OfflineHttpGetStatusCode_nid_no_patch(int request_id, int* status_code) { + (void)request_id; + (void)status_code; + return ERROR_NETWORK; +} + +int OfflineHttpInit_nid_no_patch(int memid, int ssl_ctx_id, uint64_t pool_size) { + (void)memid; + (void)ssl_ctx_id; + (void)pool_size; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_stateMutex); + g_contexts.insert(id); + return id; +} + +int OfflineHttpsDisableOption_nid_no_patch(int id, uint32_t ssl_flags) { + (void)id; + (void)ssl_flags; + return 0; +} + +int OfflineHttpSendRequest_nid_no_patch(int request_id, const void* post_data, size_t size) { + (void)request_id; + (void)post_data; + (void)size; + return ERROR_NETWORK; +} + +int OfflineHttpSetAuthEnabled_nid_no_patch(int id, int enable) { + (void)id; + (void)enable; + return 0; +} + +int OfflineHttpSetAuthInfoCallback_nid_no_patch(int id, HttpAuthInfoCallback callback, void* userArg) { + (void)id; + (void)callback; + (void)userArg; + return 0; +} + +int OfflineHttpSetCookieEnabled_nid_no_patch(int id, int enable) { + (void)id; + if (static_cast(enable) > 1) return ERROR_INVALID_VALUE; + if (enable != 0) NotImplemented_nid_no_patch("sceHttpSetCookieEnabled"); + return 0; +} + +int OfflineHttpSetAutoRedirect_nid_no_patch(int id, int enable) { + (void)id; + (void)enable; + return 0; +} + +int OfflineHttpSetConnectTimeOut_nid_no_patch(int id, uint32_t usec) { + (void)id; + (void)usec; + return 0; +} + +int OfflineHttpSetEpoll_nid_no_patch(int id, HttpEpollHandle eh, void* user_arg) { + (void)id; + (void)eh; + (void)user_arg; + return 0; +} + +int OfflineHttpSetNonblock_nid_no_patch(int id, int enable) { + (void)id; + (void)enable; + return 0; +} + +int OfflineHttpSetRecvTimeOut_nid_no_patch(int id, uint32_t usec) { + (void)id; + (void)usec; + return 0; +} + +int OfflineHttpSetRecvBlockSize_nid_no_patch(int id, uint32_t block_size) { + (void)id; + (void)block_size; + return 0; +} + +int OfflineHttpSetResponseHeaderMaxSize_nid_no_patch(int id, uint64_t header_size) { + (void)id; + (void)header_size; + return 0; +} + +int OfflineHttpSetRequestContentLength_nid_no_patch(int request_id, uint64_t content_length) { + (void)request_id; + (void)content_length; + return 0; +} + +int OfflineHttpSetResolveRetry_nid_no_patch(int id, int32_t retry) { + (void)id; + (void)retry; + return 0; +} + +int OfflineHttpSetResolveTimeOut_nid_no_patch(int id, uint32_t usec) { + (void)id; + (void)usec; + return 0; +} + +int OfflineHttpSetSendTimeOut_nid_no_patch(int id, uint32_t usec) { + (void)id; + (void)usec; + return 0; +} + +int OfflineHttpsSetMinSslVersion_nid_no_patch(int id, uint32_t ssl_version) { + (void)id; + (void)ssl_version; + return 0; +} + +int OfflineHttpsSetSslCallback_nid_no_patch(int id, HttpsCallback cbfunc, void* user_arg) { + (void)id; + (void)cbfunc; + (void)user_arg; + return 0; +} + +int OfflineHttpTerm_nid_no_patch(int http_ctx_id) { + std::vector> epolls; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; + if (!g_contexts.empty()) return 0; + for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); + g_epolls.clear(); + } + for (const auto& epoll : epolls) MarkDestroyed_nid_no_patch(*epoll); + return 0; +} + +int OfflineHttpUnsetEpoll_nid_no_patch(int id) { + (void)id; + return 0; +} + +int OfflineHttpWaitRequest_nid_no_patch(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; + epoll = FindEpoll_nid_no_patch(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::unique_lock lock(epoll->mutex); + const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; + if (timeout < 0) { + while (!woken()) epoll->wake.Wait(lock); + } else { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(timeout); + while (!woken() && std::chrono::steady_clock::now() < deadline) epoll->wake.WaitUntil(lock, deadline); + } + return epoll->aborted ? ERROR_ABORTED : 0; +} + +int OfflineHttpCreateRequestWithURL_nid_no_patch(int conn_id, int method, const char* url, uint64_t content_length) { + (void)conn_id; + (void)method; + (void)url; + (void)content_length; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int OfflineHttpReadData_nid_no_patch(int request_id, void* data, size_t size) { + (void)request_id; + (void)data; + (void)size; + return ERROR_NETWORK; +} + +int OfflineHttpRedirectCacheFlush_nid_no_patch(int http_ctx_id) { + (void)http_ctx_id; + return 0; +} + +int OfflineHttpSetChunkedTransferEnabled_nid_no_patch(int id, int enable) { + (void)id; + (void)enable; + return 0; +} + +int OfflineHttpSetInflateGZIPEnabled_nid_no_patch(int id, int enable) { + (void)id; + if (static_cast(enable) > 1) return ERROR_INVALID_VALUE; + return 0; +} + +int OfflineHttpSetRequestStatusCallback_nid_no_patch(int id, HttpRequestStatusCallback callback, void* argument) { + if (id <= 0 || id >= g_nextHandle.load(std::memory_order_relaxed)) return static_cast(0x80431100u); + std::lock_guard lock(g_statusMutex); + if (callback) g_statusCallbacks[id] = {callback, argument}; + else g_statusCallbacks.erase(id); + return 0; +} + +int OfflineHttpCreateRequest2_nid_no_patch(int conn_id, const char* method, const char* path, uint64_t content_length) { + (void)conn_id; + (void)method; + (void)path; + (void)content_length; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int OfflineHttpsEnableOption_nid_no_patch(int id, uint32_t ssl_flags) { + (void)id; + (void)ssl_flags; + return 0; +} + +int OfflineHttpsLoadCert_nid_no_patch(int http_ctx_id, int num, void* ca_list, void* cert, void* key) { + (void)http_ctx_id; + (void)num; + (void)ca_list; + (void)cert; + (void)key; + return 0; +} + +int OfflineHttpsUnloadCert_nid_no_patch(int http_ctx_id) { + (void)http_ctx_id; + return 0; +} + +int OfflineHttpGetLastErrno_nid_no_patch(int request_id, int* errno_out) { + (void)request_id; + if (errno_out == nullptr) { + return ERROR_INVALID_VALUE; + } + *errno_out = 0; + return 0; +} + +int OfflineHttpsGetSslError_nid_no_patch(int id, int* err_num, uint32_t* detail) { + (void)id; + if (err_num == nullptr || detail == nullptr) { + return ERROR_INVALID_VALUE; + } + *err_num = 0; + *detail = 0; + return 0; +} + +int OfflineHttpSetCookieRecvCallback_nid_no_patch(int id, HttpCookieRecvCallback cbfunc, void* user_arg) { + (void)id; + (void)cbfunc; + (void)user_arg; + return 0; +} + +int OfflineHttpsSetSslVersion_nid_no_patch(int id, int ssl_version) { + (void)id; + (void)ssl_version; + return 0; +} + +int OfflineHttpSetRedirectCallback_nid_no_patch(int id, HttpRedirectCallback cbfunc, void* user_arg) { + (void)id; + (void)cbfunc; + (void)user_arg; + return 0; +} + +int OfflineHttpAbortWaitRequest_nid_no_patch(HttpEpollHandle eh) { + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll_nid_no_patch(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::lock_guard lock(epoll->mutex); + epoll->aborted = true; + epoll->wake.NotifyAll(); + return 0; +} + +int OfflineHttpsGetCaList_nid_no_patch(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; + return 0; +} + +int OfflineHttpsFreeCaList_nid_no_patch(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; + return 0; +} + +} + +} diff --git a/core/libs/prx/libSceHttp/src/Offline.hpp b/core/libs/prx/libSceHttp/src/Offline.hpp new file mode 100644 index 00000000000..64d12f34db7 --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Offline.hpp @@ -0,0 +1,80 @@ +#ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_OFFLINE_HPP +#define CORE_LIBS_PRX_LIBSCEHTTP_SRC_OFFLINE_HPP + +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +using HttpRequestStatusCallback = void (APS5_VABI *)(int, int, void*); + +struct HttpsCaList { + void* certs; + int certsNum; +}; + +namespace HttpOffline { + +extern "C" { + +int OfflineHttpAbortRequest_nid_no_patch(int request_id); +int OfflineHttpAddRequestHeader_nid_no_patch(int id, const char* name, const char* value, uint32_t mode); +int OfflineHttpCreateConnection_nid_no_patch(int tmpl_id, const char* server_name, const char* scheme, uint16_t port, int enable_keep_alive); +int OfflineHttpCreateConnectionWithURL_nid_no_patch(int tmpl_id, const char* url, int enable_keep_alive); +int OfflineHttpCreateEpoll_nid_no_patch(int http_ctx_id, HttpEpollHandle* eh); +int OfflineHttpCreateRequest_nid_no_patch(int conn_id, int method, const char* path, uint64_t content_length); +int OfflineHttpCreateRequestWithURL2_nid_no_patch(int conn_id, const char* method, const char* url, uint64_t content_length); +int OfflineHttpCreateTemplate_nid_no_patch(int http_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf); +int OfflineHttpDeleteConnection_nid_no_patch(int conn_id); +int OfflineHttpDeleteRequest_nid_no_patch(int req_id); +int OfflineHttpDeleteTemplate_nid_no_patch(int tmpl_id); +int OfflineHttpDestroyEpoll_nid_no_patch(int http_ctx_id, HttpEpollHandle eh); +int OfflineHttpGetAllResponseHeaders_nid_no_patch(int request_id, char** header, size_t* header_size); +int OfflineHttpGetResponseContentLength_nid_no_patch(int request_id, int* result, uint64_t* content_length); +int OfflineHttpGetStatusCode_nid_no_patch(int request_id, int* status_code); +int OfflineHttpInit_nid_no_patch(int memid, int ssl_ctx_id, uint64_t pool_size); +int OfflineHttpsDisableOption_nid_no_patch(int id, uint32_t ssl_flags); +int OfflineHttpSendRequest_nid_no_patch(int request_id, const void* post_data, size_t size); +int OfflineHttpSetAuthEnabled_nid_no_patch(int id, int enable); +int OfflineHttpSetAuthInfoCallback_nid_no_patch(int id, HttpAuthInfoCallback callback, void* userArg); +int OfflineHttpSetCookieEnabled_nid_no_patch(int id, int enable); +int OfflineHttpSetAutoRedirect_nid_no_patch(int id, int enable); +int OfflineHttpSetConnectTimeOut_nid_no_patch(int id, uint32_t usec); +int OfflineHttpSetEpoll_nid_no_patch(int id, HttpEpollHandle eh, void* user_arg); +int OfflineHttpSetNonblock_nid_no_patch(int id, int enable); +int OfflineHttpSetRecvTimeOut_nid_no_patch(int id, uint32_t usec); +int OfflineHttpSetRecvBlockSize_nid_no_patch(int id, uint32_t block_size); +int OfflineHttpSetResponseHeaderMaxSize_nid_no_patch(int id, uint64_t header_size); +int OfflineHttpSetRequestContentLength_nid_no_patch(int request_id, uint64_t content_length); +int OfflineHttpSetResolveRetry_nid_no_patch(int id, int32_t retry); +int OfflineHttpSetResolveTimeOut_nid_no_patch(int id, uint32_t usec); +int OfflineHttpSetSendTimeOut_nid_no_patch(int id, uint32_t usec); +int OfflineHttpsSetMinSslVersion_nid_no_patch(int id, uint32_t ssl_version); +int OfflineHttpsSetSslCallback_nid_no_patch(int id, HttpsCallback cbfunc, void* user_arg); +int OfflineHttpTerm_nid_no_patch(int http_ctx_id); +int OfflineHttpUnsetEpoll_nid_no_patch(int id); +int OfflineHttpWaitRequest_nid_no_patch(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout); +int OfflineHttpCreateRequestWithURL_nid_no_patch(int conn_id, int method, const char* url, uint64_t content_length); +int OfflineHttpReadData_nid_no_patch(int request_id, void* data, size_t size); +int OfflineHttpRedirectCacheFlush_nid_no_patch(int http_ctx_id); +int OfflineHttpSetChunkedTransferEnabled_nid_no_patch(int id, int enable); +int OfflineHttpSetInflateGZIPEnabled_nid_no_patch(int id, int enable); +int OfflineHttpSetRequestStatusCallback_nid_no_patch(int id, HttpRequestStatusCallback callback, void* argument); +int OfflineHttpCreateRequest2_nid_no_patch(int conn_id, const char* method, const char* path, uint64_t content_length); +int OfflineHttpsEnableOption_nid_no_patch(int id, uint32_t ssl_flags); +int OfflineHttpsLoadCert_nid_no_patch(int http_ctx_id, int num, void* ca_list, void* cert, void* key); +int OfflineHttpsUnloadCert_nid_no_patch(int http_ctx_id); +int OfflineHttpGetLastErrno_nid_no_patch(int request_id, int* errno_out); +int OfflineHttpsGetSslError_nid_no_patch(int id, int* err_num, uint32_t* detail); +int OfflineHttpSetCookieRecvCallback_nid_no_patch(int id, HttpCookieRecvCallback cbfunc, void* user_arg); +int OfflineHttpsSetSslVersion_nid_no_patch(int id, int ssl_version); +int OfflineHttpSetRedirectCallback_nid_no_patch(int id, HttpRedirectCallback cbfunc, void* user_arg); +int OfflineHttpAbortWaitRequest_nid_no_patch(HttpEpollHandle eh); +int OfflineHttpsGetCaList_nid_no_patch(int http_ctx_id, HttpsCaList* ca_list); +int OfflineHttpsFreeCaList_nid_no_patch(int http_ctx_id, HttpsCaList* ca_list); + +} + +} + +#endif From 2636a486c2f71d880b69cadb706eac719f9cc670 Mon Sep 17 00:00:00 2001 From: Aaditya Bhusal Date: Mon, 12 Oct 2026 08:47:04 +0300 Subject: [PATCH 4/4] test(http): cover network requests and skip missing dependencies --- core/libs/CMakeLists.txt | 1 + core/libs/prx/libSceHttp/CMakeLists.txt | 16 ++ core/libs/tests/GuestHttpNetwork.cpp | 256 ++++++++++++++++++++++++ core/libs/tests/http/SkipNetwork.cpp | 6 + core/libs/tests/http/network.py | 167 ++++++++++++++++ docs/dev/BUILD.md | 4 + docs/dev/debt/libSceHttp.md | 3 +- docs/user/USAGE.md | 10 + 8 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestHttpNetwork.cpp create mode 100644 core/libs/tests/http/SkipNetwork.cpp create mode 100644 core/libs/tests/http/network.py diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 8385138261f..efadac1e14f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -282,6 +282,7 @@ if(BUILD_TESTING) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) configure_windows_unwind(guest_offline_imports_tests) add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests) + set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT "APS5_HTTP_NETWORK=0") if(Python3_Interpreter_FOUND) add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $) diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 86009295520..c145d4dae9a 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -16,6 +16,22 @@ if(BUILD_TESTING) target_link_libraries(guest_http_response_header_tests PRIVATE libSceHttp libc) configure_windows_unwind(guest_http_response_header_tests) add_test(NAME guest_http_response_header COMMAND guest_http_response_header_tests) + set_tests_properties(guest_http guest_http_response_header PROPERTIES ENVIRONMENT "APS5_HTTP_NETWORK=0") + if(WIN32) + if(Python3_Interpreter_FOUND) + add_test_executable(guest_http_network_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpNetwork.cpp) + target_include_directories(guest_http_network_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http_network_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_network_tests) + add_test(NAME guest_http_network COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/tests/http/network.py $) + else() + add_test_executable(guest_http_network_missing_python_tests ${LIBS_INCLUDE_DIR}/tests/http/SkipNetwork.cpp) + add_test(NAME guest_http_network COMMAND guest_http_network_missing_python_tests) + endif() + set_tests_properties(guest_http_network PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) set_tests_properties(guest_http guest_http_response_header PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/tests/GuestHttpNetwork.cpp b/core/libs/tests/GuestHttpNetwork.cpp new file mode 100644 index 00000000000..aab56fbd0d8 --- /dev/null +++ b/core/libs/tests/GuestHttpNetwork.cpp @@ -0,0 +1,256 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceHttpInit_nid_postfix(int, int, std::uint64_t); +int APS5_VABI sceHttpTerm_nid_postfix(int); +int APS5_VABI sceHttpCreateTemplate(int, const char*, int, int); +int APS5_VABI sceHttpCreateConnectionWithURL(int, const char*, int); +int APS5_VABI sceHttpCreateConnection(int, const char*, const char*, std::uint16_t, int); +int APS5_VABI sceHttpCreateRequest(int, int, const char*, std::uint64_t); +int APS5_VABI sceHttpCreateRequest2(int, const char*, const char*, std::uint64_t); +int APS5_VABI sceHttpCreateRequestWithURL(int, int, const char*, std::uint64_t); +int APS5_VABI sceHttpCreateRequestWithURL2(int, const char*, const char*, std::uint64_t); +int APS5_VABI sceHttpDeleteRequest(int); +int APS5_VABI sceHttpDeleteConnection(int); +int APS5_VABI sceHttpDeleteTemplate(int); +int APS5_VABI sceHttpAddRequestHeader(int, const char*, const char*, std::uint32_t); +int APS5_VABI sceHttpSendRequest(int, const void*, std::size_t); +int APS5_VABI sceHttpReadData(int, void*, std::size_t); +int APS5_VABI sceHttpGetStatusCode(int, int*); +int APS5_VABI sceHttpGetAllResponseHeaders(int, char**, std::size_t*); +int APS5_VABI sceHttpGetResponseContentLength(int, int*, std::uint64_t*); +int APS5_VABI sceHttpSetRecvTimeOut(int, std::uint32_t); +int APS5_VABI sceHttpSetConnectTimeOut(int, std::uint32_t); +int APS5_VABI sceHttpSetSendTimeOut(int, std::uint32_t); +int APS5_VABI sceHttpSetResolveTimeOut(int, std::uint32_t); +int APS5_VABI sceHttpSetRecvBlockSize(int, std::uint32_t); +int APS5_VABI sceHttpSetResponseHeaderMaxSize(int, std::uint64_t); +int APS5_VABI sceHttpSetRequestContentLength(int, std::uint64_t); +int APS5_VABI sceHttpSetAutoRedirect(int, int); +int APS5_VABI sceHttpSetInflateGZIPEnabled(int, int); +int APS5_VABI sceHttpSetCookieEnabled(int, int); +int APS5_VABI sceHttpSetNonblock(int, int); +int APS5_VABI sceHttpGetLastErrno(int, int*); +int APS5_VABI sceHttpsGetSslError(int, int*, std::uint32_t*); +int APS5_VABI sceHttpsDisableOption(int, std::uint32_t); +int APS5_VABI sceHttpAbortRequest(int); +} + +static void Require(bool value, const char* label) { + if (!value) { + std::fprintf(stderr, "HTTP network: %s\n", label); + std::abort(); + } +} + +static std::string ReadBody(int request) { + std::string result; + char buffer[32]; + for (;;) { + const auto count = sceHttpReadData(request, buffer, sizeof(buffer)); + Require(count >= 0 && count <= static_cast(sizeof(buffer)), "body read failed"); + if (!count) return result; + result.append(buffer, count); + Require(result.size() <= 4096, "body exceeded test limit"); + } +} + +static int Get(int connection, const char* path) { + const auto request = sceHttpCreateRequest(connection, 0, path, 0); + Require(request > 0, "GET request creation"); + Require(sceHttpSendRequest(request, nullptr, 0) == 0, "GET send"); + return request; +} + +static void WaitForServer(int connection, const std::string& tag) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (std::chrono::steady_clock::now() < deadline) { + const auto state = Get(connection, ("/state/" + tag).c_str()); + const auto received = ReadBody(state) == "1"; + Require(sceHttpDeleteRequest(state) == 0, "state request cleanup"); + if (received) return; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + Require(false, "server did not receive pending request"); +} + +int main(int argc, char** argv) { + Require(argc == 3, "HTTP and HTTPS base URLs required"); + constexpr int invalidId = static_cast(0x80431100u); + constexpr int invalidValue = static_cast(0x804311feu); + constexpr int aborted = static_cast(0x80431080u); + const auto context = sceHttpInit_nid_postfix(0, 0, 65536); + Require(context > 0, "context creation"); + const auto tmpl = sceHttpCreateTemplate(context, "AnyPS5-local-test", 2, 0); + Require(tmpl > 0, "template creation"); + Require(sceHttpSetConnectTimeOut(tmpl, 1000000) == 0 && sceHttpSetResolveTimeOut(tmpl, 1000000) == 0 && sceHttpSetSendTimeOut(tmpl, 1000000) == 0 && sceHttpSetRecvTimeOut(tmpl, 1000000) == 0, "timeout configuration"); + Require(sceHttpAddRequestHeader(tmpl, "X-Inherited", "template", 0) == 0, "template header"); + const auto connection = sceHttpCreateConnectionWithURL(tmpl, argv[1], 1); + Require(connection > 0, "connection creation"); + Require(sceHttpCreateTemplate(-1, "x", 2, 0) == invalidId, "invalid parent"); + Require(sceHttpCreateConnectionWithURL(tmpl, "file:///local", 0) < 0, "non-HTTP URL accepted"); + Require(sceHttpCreateConnectionWithURL(tmpl, "http://user:secret@localhost/", 0) < 0, "URL credentials accepted"); + Require(sceHttpCreateRequest(connection, 99, "/", 0) < 0, "unknown method"); + Require(sceHttpCreateRequest2(connection, "GET\r\nX: y", "/", 0) == invalidValue, "method injection"); + Require(sceHttpAddRequestHeader(connection, "X-Bad\r\n", "a", 0) == invalidValue && sceHttpAddRequestHeader(connection, "X-Bad", "a\r\nHost: other", 0) == invalidValue, "header injection"); + Require(sceHttpSetRecvBlockSize(connection, 0) == invalidValue, "zero read block"); + Require(sceHttpSetAutoRedirect(connection, -1) == invalidValue, "invalid redirect value"); + const auto before = sceHttpCreateRequest(connection, 0, "/binary", 0); + int status = 777; + Require(sceHttpGetStatusCode(before, &status) == static_cast(0x80431065u) && status == 777, "before-send status"); + Require(sceHttpSetRecvBlockSize(before, 3) == 0, "small read block"); + const auto binaryResult = sceHttpSendRequest(before, nullptr, 0); + if (binaryResult != 0) { + int error = 0; + sceHttpGetLastErrno(before, &error); + std::fprintf(stderr, "binary send result=0x%x host error=%d\n", binaryResult, error); + } + Require(binaryResult == 0, "binary send"); + Require(sceHttpGetStatusCode(before, &status) == 0 && status == 200, "binary status"); + char* headers = nullptr; + std::size_t headerSize = 0; + Require(sceHttpGetAllResponseHeaders(before, &headers, &headerSize) == 0 && headerSize == std::strlen(headers) + 1 && std::strstr(headers, "X-Test: local"), "response headers"); + int lengthResult = -1; + std::uint64_t length = 999; + Require(sceHttpGetResponseContentLength(before, &lengthResult, &length) == 0 && lengthResult == 0 && length == 8, "binary content length"); + char small[32]; + Require(sceHttpReadData(before, small, sizeof(small)) == 3 && std::memcmp(small, "abc", 3) == 0, "read block cap"); + Require(ReadBody(before) == std::string("\0tail", 5), "binary body and partial reads"); + Require(sceHttpReadData(before, nullptr, 0) == 0, "empty read"); + Require(sceHttpSendRequest(before, nullptr, 0) == static_cast(0x80431066u), "send after send"); + Require(sceHttpDeleteRequest(before) == 0 && sceHttpGetStatusCode(before, &status) == invalidId, "deleted request"); + + const auto chunked = Get(connection, "/chunked"); + Require(sceHttpGetResponseContentLength(chunked, &lengthResult, &length) == 0 && lengthResult == 1 && length == 0, "unknown response length"); + Require(ReadBody(chunked) == "firstsecond", "chunk framing leaked into body"); + Require(sceHttpDeleteRequest(chunked) == 0, "chunked cleanup"); + const auto post = sceHttpCreateRequestWithURL2(connection, "POST", (std::string(argv[1]) + "/echo").c_str(), 0); + const std::string payload("post\0bytes", 10); + Require(sceHttpSendRequest(post, payload.data(), payload.size()) == invalidValue, "body length mismatch"); + Require(sceHttpSetRequestContentLength(post, payload.size()) == 0, "content length update"); + Require(sceHttpSendRequest(post, payload.data(), payload.size()) == 0 && ReadBody(post) == payload, "POST echo"); + Require(sceHttpDeleteRequest(post) == 0, "POST cleanup"); + const auto head = sceHttpCreateRequest2(connection, "HEAD", "/binary", 0); + Require(sceHttpSendRequest(head, nullptr, 0) == 0 && ReadBody(head).empty(), "HEAD body"); + Require(sceHttpGetResponseContentLength(head, &lengthResult, &length) == 0 && length == 8, "HEAD length"); + Require(sceHttpDeleteRequest(head) == 0, "HEAD cleanup"); + const auto missing = Get(connection, "/missing"); + Require(sceHttpGetStatusCode(missing, &status) == 0 && status == 404 && ReadBody(missing) == "missing", "HTTP error is a response"); + Require(sceHttpDeleteRequest(missing) == 0, "404 cleanup"); + const auto disconnected = sceHttpCreateRequest(connection, 0, "/disconnect", 0); + Require(sceHttpSendRequest(disconnected, nullptr, 0) < 0, "connection failure accepted"); + int disconnectError = 0; + Require(sceHttpGetLastErrno(disconnected, &disconnectError) == 0 && disconnectError != 0, "connection failure diagnostics"); + Require(sceHttpDeleteRequest(disconnected) == 0, "disconnected cleanup"); + const auto redirect = Get(connection, "/redirect"); + Require(sceHttpGetStatusCode(redirect, &status) == 0 && status == 200 && ReadBody(redirect) == "final", "redirect following"); + Require(sceHttpDeleteRequest(redirect) == 0, "redirect cleanup"); + const auto noRedirect = sceHttpCreateRequest(connection, 0, "/redirect", 0); + Require(sceHttpSetAutoRedirect(noRedirect, 0) == 0 && sceHttpSendRequest(noRedirect, nullptr, 0) == 0, "redirect-disabled send"); + Require(sceHttpGetStatusCode(noRedirect, &status) == 0 && status == 302 && ReadBody(noRedirect).empty(), "redirect-disabled response"); + Require(sceHttpDeleteRequest(noRedirect) == 0, "redirect-disabled cleanup"); + for (const auto* header : {"aUtHoRiZaTiOn", "Proxy-Authorization", "Cookie"}) { + const auto credentialTemplate = sceHttpCreateTemplate(context, "AnyPS5-local-test", 2, 0); + Require(sceHttpAddRequestHeader(credentialTemplate, header, "local-test", 0) == 0, "inherited credential header setup"); + const auto credentialConnection = sceHttpCreateConnectionWithURL(credentialTemplate, argv[1], 0); + const auto credentials = sceHttpCreateRequest(credentialConnection, 0, "/credentials", 0); + bool rejected = false; + try { sceHttpSendRequest(credentials, nullptr, 0); } catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "credential headers accepted with automatic redirects"); + Require(sceHttpSetAutoRedirect(credentials, 0) == 0 && sceHttpSendRequest(credentials, nullptr, 0) == 0 && ReadBody(credentials) == "local-test", "credential request without automatic redirects"); + Require(sceHttpDeleteTemplate(credentialTemplate) == 0, "credential request cleanup"); + } + const auto gzip = sceHttpCreateRequest(connection, 0, "/gzip", 0); + Require(sceHttpSetInflateGZIPEnabled(gzip, 1) == 0 && sceHttpSendRequest(gzip, nullptr, 0) == 0 && ReadBody(gzip) == "compressed body", "GZIP decoding"); + Require(sceHttpDeleteRequest(gzip) == 0, "GZIP cleanup"); + const auto headerRequest = sceHttpCreateRequest(connection, 0, "/headers", 0); + Require(sceHttpAddRequestHeader(headerRequest, "X-Value", "old", 0) == 0 && sceHttpAddRequestHeader(headerRequest, "x-value", "new", 0) == 0 && sceHttpAddRequestHeader(headerRequest, "X-Value", "added", 1) == 0, "header overwrite and add"); + Require(sceHttpSendRequest(headerRequest, nullptr, 0) == 0 && ReadBody(headerRequest) == "template|new,added|AnyPS5-local-test", "inherited and repeated headers"); + Require(sceHttpDeleteRequest(headerRequest) == 0, "header cleanup"); + const auto huge = sceHttpCreateRequest(connection, 0, "/largeheaders", 0); + Require(sceHttpSetResponseHeaderMaxSize(huge, 512) == 0, "response header cap"); + Require(sceHttpSendRequest(huge, nullptr, 0) == static_cast(0x80431073u), "large header accepted"); + Require(sceHttpDeleteRequest(huge) == 0, "header-limit cleanup"); + + const auto tlsConnection = sceHttpCreateConnectionWithURL(tmpl, argv[2], 0); + const auto badTls = sceHttpCreateRequest(tlsConnection, 0, "/final", 0); + Require(sceHttpSendRequest(badTls, nullptr, 0) == static_cast(0x80431075u), "untrusted TLS certificate accepted by default"); + int hostError = 0; + std::uint32_t sslDetail = 0; + Require(sceHttpsGetSslError(badTls, &hostError, &sslDetail) == 0 && hostError != 0 && sslDetail != 0, "TLS diagnostics"); + Require(sceHttpDeleteRequest(badTls) == 0, "TLS failure cleanup"); + const auto tls = sceHttpCreateRequestWithURL(tlsConnection, 0, (std::string(argv[2]) + "/final").c_str(), 0); + Require(sceHttpsDisableOption(tls, 0x20) == 0, "explicit test CA-check override"); + const auto tlsResult = sceHttpSendRequest(tls, nullptr, 0); + if (tlsResult != 0) { + sceHttpsGetSslError(tls, &hostError, &sslDetail); + std::fprintf(stderr, "TLS send result=0x%x host error=%d ssl detail=0x%x\n", tlsResult, hostError, sslDetail); + } + Require(tlsResult == 0 && ReadBody(tls) == "final", "HTTPS transfer"); + Require(sceHttpDeleteRequest(tls) == 0, "HTTPS cleanup"); + + const auto timeout = sceHttpCreateRequest(connection, 0, "/slowheaders/timeout", 0); + Require(sceHttpSetRecvTimeOut(timeout, 100000) == 0, "receive timeout"); + const auto timeoutResult = sceHttpSendRequest(timeout, nullptr, 0); + if (timeoutResult != static_cast(0x80431068u)) std::fprintf(stderr, "timeout result=0x%x\n", timeoutResult); + Require(timeoutResult == static_cast(0x80431068u), "timeout result"); + Require(sceHttpGetLastErrno(timeout, &hostError) == 0 && hostError != 0, "timeout diagnostics"); + Require(sceHttpDeleteRequest(timeout) == 0, "timeout cleanup"); + const auto bodyTimeout = sceHttpCreateRequest(connection, 0, "/slowbody/timeout", 0); + Require(sceHttpSetRecvTimeOut(bodyTimeout, 100000) == 0 && sceHttpSendRequest(bodyTimeout, nullptr, 0) == 0, "body timeout setup"); + Require(sceHttpReadData(bodyTimeout, small, sizeof(small)) == static_cast(0x80431068u), "body receive timeout"); + Require(sceHttpReadData(bodyTimeout, small, sizeof(small)) == static_cast(0x80431068u), "body timeout remains failed"); + Require(sceHttpDeleteRequest(bodyTimeout) == 0, "body timeout cleanup"); + const auto preAborted = sceHttpCreateRequest(connection, 0, "/final", 0); + Require(sceHttpAbortRequest(preAborted) == 0 && sceHttpSendRequest(preAborted, nullptr, 0) == aborted, "abort before send"); + Require(sceHttpDeleteRequest(preAborted) == 0, "pre-aborted cleanup"); + const auto isolatedA = sceHttpCreateRequest(connection, 0, "/headers", 0); + const auto isolatedB = sceHttpCreateRequest(connection, 0, "/headers", 0); + Require(sceHttpAddRequestHeader(isolatedA, "X-Value", "A", 0) == 0 && sceHttpAddRequestHeader(isolatedB, "X-Value", "B", 0) == 0, "isolated headers setup"); + auto sendA = std::async(std::launch::async, [&] { return sceHttpSendRequest(isolatedA, nullptr, 0); }); + auto sendB = std::async(std::launch::async, [&] { return sceHttpSendRequest(isolatedB, nullptr, 0); }); + Require(sendA.get() == 0 && sendB.get() == 0 && ReadBody(isolatedA) == "template|A|AnyPS5-local-test" && ReadBody(isolatedB) == "template|B|AnyPS5-local-test", "simultaneous requests share state"); + Require(sceHttpDeleteRequest(isolatedA) == 0 && sceHttpDeleteRequest(isolatedB) == 0, "isolated requests cleanup"); + for (int i = 0; i < 5; ++i) { + const auto tag = "abort" + std::to_string(i); + const auto pending = sceHttpCreateRequest(connection, 0, ("/slowheaders/" + tag).c_str(), 0); + Require(sceHttpSetRecvTimeOut(pending, 0) == 0, "infinite receive wait setup"); + auto send = std::async(std::launch::async, [&] { return sceHttpSendRequest(pending, nullptr, 0); }); + WaitForServer(connection, tag); + Require(sceHttpSendRequest(pending, nullptr, 0) == static_cast(0x80431021u), "concurrent request operation"); + Require(sceHttpAbortRequest(pending) == 0 && sceHttpAbortRequest(pending) == 0, "abort is repeatable"); + Require(send.wait_for(std::chrono::seconds(1)) == std::future_status::ready && send.get() == aborted, "abort did not wake send"); + Require(sceHttpDeleteRequest(pending) == 0, "aborted cleanup"); + } + const auto pendingRead = Get(connection, "/slowbody/read"); + auto read = std::async(std::launch::async, [&] { char byte; return sceHttpReadData(pendingRead, &byte, 1); }); + Require(read.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout, "slow body was not pending"); + Require(sceHttpAbortRequest(pendingRead) == 0, "abort pending read"); + Require(read.wait_for(std::chrono::seconds(1)) == std::future_status::ready && read.get() == aborted, "abort did not wake read"); + Require(sceHttpDeleteRequest(pendingRead) == 0, "aborted read cleanup"); + const auto child = sceHttpCreateConnectionWithURL(tmpl, argv[1], 0); + const auto pendingDelete = sceHttpCreateRequest(child, 0, "/slowheaders/delete", 0); + auto deletedSend = std::async(std::launch::async, [&] { return sceHttpSendRequest(pendingDelete, nullptr, 0); }); + WaitForServer(connection, "delete"); + Require(sceHttpDeleteConnection(child) == 0, "delete active parent"); + Require(deletedSend.wait_for(std::chrono::seconds(1)) == std::future_status::ready && deletedSend.get() == aborted, "parent delete did not cancel child"); + Require(sceHttpDeleteRequest(pendingDelete) == invalidId, "deleted descendant survives"); + + bool unsupported = false; + try { sceHttpSetNonblock(connection, 1); } catch (const std::runtime_error&) { unsupported = true; } + Require(unsupported, "nonblocking silently accepted"); + unsupported = false; + try { sceHttpSetCookieEnabled(connection, 1); } catch (const std::runtime_error&) { unsupported = true; } + Require(unsupported, "cookies silently accepted"); + Require(sceHttpTerm_nid_postfix(context) == 0 && sceHttpDeleteTemplate(tmpl) == invalidId && sceHttpTerm_nid_postfix(context) == invalidId, "context teardown invalidates descendants"); + std::puts("HTTP network checks passed"); +} diff --git a/core/libs/tests/http/SkipNetwork.cpp b/core/libs/tests/http/SkipNetwork.cpp new file mode 100644 index 00000000000..9e6b0585bb8 --- /dev/null +++ b/core/libs/tests/http/SkipNetwork.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + std::puts("guest_http_network: skipped, Python 3 is unavailable"); + return 77; +} diff --git a/core/libs/tests/http/network.py b/core/libs/tests/http/network.py new file mode 100644 index 00000000000..b9f59055a37 --- /dev/null +++ b/core/libs/tests/http/network.py @@ -0,0 +1,167 @@ +import gzip +import http.server +import os +from pathlib import Path +import shutil +import ssl +import subprocess +import sys +import tempfile +import threading +import time + + +pending = set() +pending_lock = threading.Lock() + + +class Server(http.server.ThreadingHTTPServer): + daemon_threads = True + + def finish_request(self, request, client_address): + request.settimeout(3) + try: + if self.tls_context: + request = self.tls_context.wrap_socket(request, server_side=True) + with request: + self.RequestHandlerClass(request, client_address, self) + except (TimeoutError, ConnectionError, ssl.SSLError): + request.close() + + +class Handler(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, *_): + pass + + def handle(self): + try: + super().handle() + except (BrokenPipeError, ConnectionResetError, ssl.SSLError): + pass + + def response(self, status, body, headers=()): + self.send_response(status) + self.send_header("Content-Length", str(len(body))) + for name, value in headers: + self.send_header(name, value) + self.end_headers() + if self.command != "HEAD": + self.wfile.write(body) + + def do_HEAD(self): + self.do_GET() + + def do_POST(self): + length = int(self.headers.get("Content-Length", "0")) + self.response(200, self.rfile.read(length)) + + def do_GET(self): + try: + if self.path == "/binary": + self.response(200, b"abc\0tail", [("X-Test", "local")]) + elif self.path == "/final": + self.response(200, b"final") + elif self.path == "/missing": + self.response(404, b"missing") + elif self.path == "/disconnect": + self.close_connection = True + elif self.path == "/redirect": + self.response(302, b"", [("Location", "/final")]) + elif self.path == "/headers": + body = "|".join([self.headers.get("X-Inherited", ""), ",".join(self.headers.get_all("X-Value", [])), self.headers.get("User-Agent", "")]) + self.response(200, body.encode()) + elif self.path == "/credentials": + body = self.headers.get("Authorization") or self.headers.get("Proxy-Authorization") or self.headers.get("Cookie", "") + self.response(200, body.encode()) + elif self.path == "/largeheaders": + self.response(200, b"", [("X-Large", "x" * 4096)]) + elif self.path == "/gzip": + self.response(200, gzip.compress(b"compressed body"), [("Content-Encoding", "gzip")]) + elif self.path == "/chunked": + self.send_response(200) + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + self.wfile.write(b"5\r\nfirst\r\n6\r\nsecond\r\n0\r\n\r\n") + elif self.path.startswith("/state/"): + with pending_lock: + seen = self.path.rsplit("/", 1)[1] in pending + self.response(200, b"1" if seen else b"0") + elif self.path.startswith("/slowheaders/"): + with pending_lock: + pending.add(self.path.rsplit("/", 1)[1]) + time.sleep(2) + self.response(200, b"late") + elif self.path.startswith("/slowbody/"): + self.send_response(200) + self.send_header("Content-Length", "1") + self.end_headers() + self.wfile.flush() + time.sleep(2) + self.wfile.write(b"x") + else: + self.response(404, b"unexpected path") + except (BrokenPipeError, ConnectionResetError, ssl.SSLError): + pass + + +def openssl_command(): + openssl = shutil.which("openssl") + if not openssl: + git = shutil.which("git") + candidate = Path(git).resolve().parent.parent / "usr" / "bin" / "openssl.exe" if git else None + if candidate and candidate.is_file(): + openssl = str(candidate) + return openssl + + +def certificate(folder, openssl): + try: + subprocess.run([openssl, "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-sha256", + "-days", "1", "-subj", "/CN=localhost", "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1", + "-keyout", str(folder / "localhost-key.pem"), "-out", str(folder / "localhost.pem")], + check=True, capture_output=True, timeout=10) + except subprocess.CalledProcessError as error: + sys.stderr.write(error.stderr.decode(errors="replace")) + raise + + +def main(): + openssl = openssl_command() + if not openssl: + print("guest_http_network: skipped, OpenSSL is unavailable") + return 77 + temporary = tempfile.TemporaryDirectory(prefix="anyps5-http-") + folder = Path(temporary.name) + servers = [] + threads = [] + try: + certificate(folder, openssl) + for secure in (False, True): + server = Server(("127.0.0.1", 0), Handler) + server.tls_context = None + if secure: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.maximum_version = ssl.TLSVersion.TLSv1_2 + context.load_cert_chain(folder / "localhost.pem", folder / "localhost-key.pem") + server.tls_context = context + servers.append(server) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + threads.append(thread) + environment = os.environ.copy() + environment["APS5_HTTP_NETWORK"] = "1" + result = subprocess.run([sys.argv[1], f"http://127.0.0.1:{servers[0].server_port}", f"https://127.0.0.1:{servers[1].server_port}"], env=environment, timeout=15) + return 0 if result.returncode == 0 else 1 + finally: + for server in servers: + server.shutdown() + server.server_close() + for thread in threads: + thread.join(timeout=1) + temporary.cleanup() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 0b1680c82a5..55675eef488 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -190,3 +190,7 @@ ctest --test-dir build/register-context --output-on-failure ``` On Linux x86-64, omit `CMAKE_OSX_ARCHITECTURES`; the full `BUILD_TESTING` build also registers these tests. Apple silicon runs the x86-64 tests through Rosetta. They cover the unwinder's register transfer, not the complete macOS runtime or guest exception handling. + +## HTTP network test + +Python 3 and the OpenSSL command-line tool are optional dependencies for the Windows `guest_http_network` test; CTest skips it if either is unavailable. OpenSSL is also searched for in the standard Git for Windows location. Each run generates a temporary localhost certificate and key; none is committed or installed in the certificate store. Certificate generation errors and test failures are not skipped. diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 25cae419d88..d928055cf2c 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceHttp](../../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`; unlike shadPS4 they do not check that `http_ctx_id` is a live context nor that the library is initialised, and `sceHttpsFreeCaList` also nulls `certs`, which shadPS4 leaves untouched. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title +- [libSceHttp](../../../core/libs/prx/libSceHttp/Export.cpp) - with the default offline backend, no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`; unlike shadPS4 they do not check that `http_ctx_id` is a live context nor that the library is initialised, and `sceHttpsFreeCaList` also nulls `certs`, which shadPS4 leaves untouched. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title ### Unknown function info @@ -18,6 +18,7 @@ - [sceHttpSetRedirectCallback, sceHttpSetCookieRecvCallback, sceHttpsSetSslVersion, sceHttpsGetSslError](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), which only logs and returns 0; a null `errNum` or `detail` in `sceHttpsGetSslError` is assumed to return `SCE_HTTP_ERROR_INVALID_VALUE` like `sceHttpGetLastErrno` - [sceHttpSetAuthInfoCallback](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature and callback type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4); no PS5 reference has them. The `id` is not checked, as in the other libSceHttp setters - [sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes +- [libSceHttp host networking](../../../core/libs/prx/libSceHttp/src/Network.cpp) - `APS5_HTTP_NETWORK=1` enables a Windows-only WinHTTP backend. HTTP method values, header modes, TLS flags and added error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) and its [error definitions](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http_error.h), not PS5 measurements. Child handles snapshot parent settings; deleting a parent invalidates and aborts descendants. These lifecycle rules, initialization arguments, memory-pool accounting, response length after decompression and title compatibility remain unverified. The supplied pool size is validated but does not constrain host allocations. Each request owns a WinHTTP session; connections are not pooled across requests. Automatic redirects with guest-supplied Authorization, Proxy-Authorization or Cookie headers throw; disable redirects before sending such headers. Other sensitive custom headers require redirect control by the caller because WinHTTP transfers them unchanged across origins. WinHTTP supplies redirect limits, HTTP parsing, DNS retries, certificate trust and TLS negotiation. HTTPS-to-HTTP redirects are forbidden. Microsecond timeouts round up to milliseconds for WinHTTP; an additional receive deadline covers pending completion after request transmission and body reads. Native cancellation waits for the final handle-closing notification before releasing callback state or guest buffers. Native handle closure failure terminates the process because releasing buffers or callback state without confirmed closure is unsafe, following [WinHTTP concurrency rules](https://learn.microsoft.com/en-us/windows/win32/winhttp/concurrency-in-winhttp). Host errors and TLS detail masks are exposed as WinHTTP values, not verified console values. Proxy configuration, cookies, authentication, nonblocking/epoll, callbacks, custom certificates, explicit TLS versions, independent certificate date checks, redirect cache control and chunked uploads throw. Certificate-loading calls and nonnull request-status callback registrations throw before a request is sent, so titles that use them during initialization may stop there. Linux and macOS remain offline; requesting the host backend there throws. The Windows guest-ABI network test is skipped if Python 3 or OpenSSL is unavailable. Each run generates a temporary self-signed localhost certificate and key; the successful TLS transfer disables only CA verification. ### Functional diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 1e736e2820f..3deb6d9a72d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -78,6 +78,16 @@ The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, `--trap-sdk-syscall-stubs` is also for debugging. It preserves a runtime failure if a syscall wrapper is called; it does not implement FreeBSD syscalls or make these applications generally safe to run. +## HTTP networking + +Host networking is intended for forks. Upstream keeps the console offline and does not accept a host-backed `libSceHttp` on `main` ([decision](https://github.com/boykopovar/AnyPS5/issues/3844#issuecomment-6115108934)). + +On Windows, `APS5_HTTP_NETWORK=1` enables experimental host HTTP/HTTPS for `libSceHttp`. Set the environment variable before starting the title. An unset variable or `0` keeps the offline backend; `1` on Linux or macOS throws. `libSceHttp2` remains offline. + +The backend supports blocking requests, binary bodies, response headers and status, chunked responses, optional GZIP decoding, redirects, timeouts and cancellation. HTTPS uses Windows certificate trust and checks certificates by default. Disable automatic redirects before sending Authorization, Proxy-Authorization, Cookie or other sensitive custom headers; credential headers with automatic redirects throw. + +With the host backend enabled, certificate-loading calls, epoll calls, enabling nonblocking requests and registering a nonnull request-status callback throw. Titles that load custom certificates during initialization may stop before their first request. Automatic proxy configuration, cookies, authentication, other guest callbacks, explicit TLS versions and chunked uploads also throw when requested. Console behavior and title compatibility remain unverified; see [technical debt](../dev/debt/libSceHttp.md). + ## Runtime layout For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output.