#include #include #include #include #include #include #include #include #include #include #include #include #include "prx/libSceHttp2/include/Http2.hpp" #include "SceTypes.hpp" #include "Cookies.hpp" namespace { constexpr int ERROR_NOT_FOUND = static_cast(0x817B1025); constexpr int ERROR_INVALID_ID = static_cast(0x817B1100); constexpr int ERROR_OUT_OF_SIZE = static_cast(0x817B1104); constexpr int ERROR_INVALID_VALUE = static_cast(0x817B11FE); constexpr int ERROR_REJECTED = static_cast(0x817B1219); constexpr int ERROR_INVALID_URL = static_cast(0x817B3060); constexpr std::uint32_t DEFAULT_MAX_SIZE = 4096; constexpr std::uint32_t DEFAULT_MAX_NUM_PER_DOMAIN = 50; constexpr std::uint32_t DEFAULT_MAX_NUM = 3000; constexpr std::size_t EVICTED_OVER_MAX_NUM = 10; constexpr std::size_t HEADER_SLACK = 3; constexpr std::int64_t NEVER = std::numeric_limits::max(); constexpr std::int64_t EARLIEST = std::numeric_limits::min(); struct Cookie { std::string name; std::string value; std::string domain; std::string path; std::int64_t expiry; bool hasValue; bool secure; }; struct CookieBox { std::vector cookies; std::uint32_t maxNum = DEFAULT_MAX_NUM; std::uint32_t maxNumPerDomain = DEFAULT_MAX_NUM_PER_DOMAIN; std::uint32_t maxSize = DEFAULT_MAX_SIZE; std::size_t maxInuseSize = 0; std::uint32_t maxInuseNum = 0; std::uint32_t removedNum = 0; }; struct Url { bool secure; std::string host; std::string path; }; std::mutex g_cookiesMutex; std::unordered_map g_cookieBoxes; std::int64_t Now() { return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } char Lower(char c) { return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; } std::string LowerCopy(std::string_view text) { std::string lowered(text); std::transform(lowered.begin(), lowered.end(), lowered.begin(), Lower); return lowered; } bool EqualsIgnoreCase(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); }); } std::string_view Trim(std::string_view text) { while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) text.remove_prefix(1); while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) text.remove_suffix(1); return text; } int ParseUrl(std::string_view text, Url& url) { const std::size_t separator = text.find("://"); url.secure = separator != std::string_view::npos && EqualsIgnoreCase(text.substr(0, separator), "https"); std::string_view rest = separator == std::string_view::npos ? text : text.substr(separator + 3); const std::size_t authorityEnd = std::min(rest.find_first_of("/?#"), rest.size()); std::string_view authority = rest.substr(0, authorityEnd); if (const std::size_t at = authority.rfind('@'); at != std::string_view::npos) authority.remove_prefix(at + 1); if (!authority.empty() && authority.front() == '[') { const std::size_t close = authority.find(']'); if (close == std::string_view::npos) return ERROR_INVALID_URL; authority = authority.substr(0, close + 1); } else if (const std::size_t colon = authority.find(':'); colon != std::string_view::npos) { authority = authority.substr(0, colon); } if (authority.empty()) return ERROR_REJECTED; url.host = LowerCopy(authority); std::string_view path = rest.substr(authorityEnd); path = path.substr(0, std::min(path.find_first_of("?#"), path.size())); url.path = path.empty() || path.front() != '/' ? "/" : std::string(path); return 0; } std::string DefaultPath(const std::string& path) { const std::size_t slash = path.rfind('/'); return slash == std::string::npos ? "/" : path.substr(0, slash + 1); } bool IsIpAddress(const std::string& host) { return host.front() == '[' || host.find_first_not_of("0123456789.") == std::string::npos; } bool DomainMatches(const std::string& host, const std::string& domain) { if (host == domain) return true; return host.size() > domain.size() && host.compare(host.size() - domain.size(), domain.size(), domain) == 0 && host[host.size() - domain.size() - 1] == '.' && !IsIpAddress(host); } bool PathMatches(const std::string& requestPath, const std::string& cookiePath) { return requestPath.compare(0, cookiePath.size(), cookiePath) == 0; } bool IsDateDelimiter(char c) { const auto u = static_cast(c); return u == 0x09 || (u >= 0x20 && u <= 0x2F) || (u >= 0x3B && u <= 0x40) || (u >= 0x5B && u <= 0x60) || (u >= 0x7B && u <= 0x7E); } bool IsDigit(char c) { return c >= '0' && c <= '9'; } bool LeadingDigits(std::string_view token, std::size_t minimum, std::size_t maximum, int& value, std::size_t& used) { used = 0; value = 0; while (used < token.size() && IsDigit(token[used])) value = value * 10 + (token[used++] - '0'); return used >= minimum && used <= maximum; } bool ParseTime(std::string_view token, int& hour, int& minute, int& second) { int* fields[3] = {&hour, &minute, &second}; for (int i = 0; i < 3; ++i) { std::size_t used = 0; if (!LeadingDigits(token, 1, 2, *fields[i], used)) return false; token.remove_prefix(used); if (i < 2) { if (token.empty() || token.front() != ':') return false; token.remove_prefix(1); } } return true; } std::int64_t DaysFromCivil(int year, int month, int day) { year -= month <= 2; const std::int64_t era = (year >= 0 ? year : year - 399) / 400; const std::int64_t yearOfEra = year - era * 400; const std::int64_t dayOfYear = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; return era * 146097 + dayOfEra - 719468; } std::optional ParseCookieDate(std::string_view text) { static constexpr const char* MONTHS[12] = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}; bool foundTime = false; bool foundDay = false; bool foundMonth = false; bool foundYear = false; int hour = 0, minute = 0, second = 0, day = 0, month = 0, year = 0; std::size_t position = 0; while (position < text.size()) { while (position < text.size() && IsDateDelimiter(text[position])) ++position; const std::size_t start = position; while (position < text.size() && !IsDateDelimiter(text[position])) ++position; const std::string_view token = text.substr(start, position - start); if (token.empty()) continue; int value = 0; std::size_t used = 0; if (!foundTime && ParseTime(token, hour, minute, second)) { foundTime = true; } else if (!foundDay && LeadingDigits(token, 1, 2, value, used) && (used == token.size() || !IsDigit(token[used]))) { foundDay = true; day = value; } else if (const auto* name = token.size() >= 3 ? std::find_if(std::begin(MONTHS), std::end(MONTHS), [&](const char* candidate) { return EqualsIgnoreCase(token.substr(0, 3), candidate); }) : std::end(MONTHS); !foundMonth && name != std::end(MONTHS)) { foundMonth = true; month = 1 + static_cast(name - std::begin(MONTHS)); } else if (!foundYear && LeadingDigits(token, 2, 4, value, used) && (used == token.size() || !IsDigit(token[used]))) { foundYear = true; year = value; } } if (foundYear && year >= 70 && year <= 99) year += 1900; if (foundYear && year >= 0 && year <= 69) year += 2000; if (!foundTime || !foundDay || !foundMonth || !foundYear) return std::nullopt; if (day < 1 || day > 31 || year < 1601 || hour > 23 || minute > 59 || second > 59) return std::nullopt; const std::int64_t monthStart = DaysFromCivil(year, month, 1); const std::int64_t nextMonthStart = month < 12 ? DaysFromCivil(year, month + 1, 1) : DaysFromCivil(year + 1, 1, 1); if (day > nextMonthStart - monthStart) return std::nullopt; return DaysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second; } std::optional ParseMaxAge(std::string_view text, std::int64_t now) { const bool negative = !text.empty() && text.front() == '-'; if (negative) text.remove_prefix(1); if (text.empty() || !std::all_of(text.begin(), text.end(), IsDigit)) return std::nullopt; if (negative) return EARLIEST; std::int64_t seconds = 0; for (const char c : text) { if (seconds > (NEVER - (c - '0')) / 10) return NEVER; seconds = seconds * 10 + (c - '0'); } if (seconds == 0) return EARLIEST; return seconds > NEVER - now ? NEVER : now + seconds; } std::size_t PairSize(const Cookie& cookie) { return cookie.name.size() + cookie.value.size(); } std::size_t StoredSize(const Cookie& cookie) { return PairSize(cookie) + cookie.domain.size() + cookie.path.size() + 5; } std::size_t InuseSize(const CookieBox& box) { std::size_t size = 0; for (const Cookie& cookie : box.cookies) size += StoredSize(cookie); return size; } void RemoveExpired(CookieBox& box, std::int64_t now) { box.cookies.erase(std::remove_if(box.cookies.begin(), box.cookies.end(), [&](const Cookie& cookie) { return cookie.expiry <= now; }), box.cookies.end()); } void MakeRoom(CookieBox& box, const std::string& domain) { const auto inDomain = [&](const Cookie& cookie) { return cookie.domain == domain; }; while (true) { const auto count = static_cast(std::count_if(box.cookies.begin(), box.cookies.end(), inDomain)); if (count == 0 || count < box.maxNumPerDomain) break; box.cookies.erase(std::find_if(box.cookies.begin(), box.cookies.end(), inDomain)); ++box.removedNum; } if (box.cookies.size() >= box.maxNum) { const std::size_t evicted = std::min(box.cookies.size(), EVICTED_OVER_MAX_NUM); box.cookies.erase(box.cookies.begin(), box.cookies.begin() + static_cast(evicted)); box.removedNum += static_cast(evicted); } } void RecordUsage(CookieBox& box) { box.maxInuseSize = std::max(box.maxInuseSize, InuseSize(box)); box.maxInuseNum = std::max(box.maxInuseNum, static_cast(box.cookies.size())); } int StoreCookie(CookieBox& box, const Url& url, std::string_view text, std::int64_t now) { const std::size_t pairEnd = std::min(text.find(';'), text.size()); const std::string_view pair = text.substr(0, pairEnd); const std::size_t equals = pair.find('='); Cookie cookie{}; cookie.name = std::string(Trim(pair.substr(0, equals))); cookie.hasValue = equals != std::string_view::npos; if (cookie.hasValue) cookie.value = std::string(Trim(pair.substr(equals + 1))); cookie.expiry = NEVER; if (PairSize(cookie) > box.maxSize || box.maxSize == 0) return ERROR_OUT_OF_SIZE; std::optional maxAge; std::optional expires; std::string domain; std::optional path; std::string_view attributes = text.substr(pairEnd); while (!attributes.empty()) { attributes.remove_prefix(1); const std::size_t end = std::min(attributes.find(';'), attributes.size()); const std::string_view attribute = attributes.substr(0, end); attributes.remove_prefix(end); const std::size_t split = attribute.find('='); const std::string_view key = Trim(attribute.substr(0, split)); const std::string_view value = split == std::string_view::npos ? std::string_view{} : Trim(attribute.substr(split + 1)); if (EqualsIgnoreCase(key, "expires")) { if (const auto parsed = ParseCookieDate(value)) expires = parsed; } else if (EqualsIgnoreCase(key, "max-age")) { if (const auto parsed = ParseMaxAge(value, now)) maxAge = parsed; } else if (EqualsIgnoreCase(key, "domain")) { if (!value.empty()) domain = LowerCopy(value.front() == '.' ? value.substr(1) : value); } else if (EqualsIgnoreCase(key, "path")) { path = !value.empty() && value.front() == '/' ? std::string(value) : DefaultPath(url.path); } else if (EqualsIgnoreCase(key, "secure")) { cookie.secure = true; } } if (maxAge) cookie.expiry = *maxAge; else if (expires) cookie.expiry = *expires; if (!domain.empty() && !DomainMatches(url.host, domain)) return ERROR_REJECTED; cookie.domain = domain.empty() ? url.host : domain; cookie.path = path ? *path : DefaultPath(url.path); const auto existing = std::find_if(box.cookies.begin(), box.cookies.end(), [&](const Cookie& stored) { return stored.name == cookie.name && stored.domain == cookie.domain && stored.path == cookie.path; }); if (existing != box.cookies.end()) box.cookies.erase(existing); if (cookie.expiry <= now) return 0; RemoveExpired(box, now); MakeRoom(box, cookie.domain); box.cookies.push_back(std::move(cookie)); RecordUsage(box); return 0; } void AppendCookie(std::string& header, const Cookie& cookie) { if (!header.empty()) header += "; "; if (cookie.name.empty()) { header += cookie.value; return; } header += cookie.name; if (!cookie.hasValue) return; header += '='; header += cookie.value; } CookieBox* FindBox(int cookieBoxId) { if (!Http2CookieBoxExists(cookieBoxId)) return nullptr; return &g_cookieBoxes[cookieBoxId]; } } void Http2ForgetCookies(int cookieBoxId) { std::lock_guard lock(g_cookiesMutex); g_cookieBoxes.erase(cookieBoxId); } void Http2FlushCookies(int cookieBoxId) { std::lock_guard lock(g_cookiesMutex); const auto box = g_cookieBoxes.find(cookieBoxId); if (box != g_cookieBoxes.end()) box->second.cookies.clear(); } extern "C" { int APS5_VABI sceHttp2AddCookie(int cookieBoxId, const char* url, const char* cookie, std::size_t cookieLength) { std::lock_guard lock(g_cookiesMutex); CookieBox* box = FindBox(cookieBoxId); if (!box) return ERROR_INVALID_ID; if (!url || !cookie || cookieLength == 0) return ERROR_INVALID_VALUE; Url parsed{}; if (const int result = ParseUrl(url, parsed); result != 0) return result; return StoreCookie(*box, parsed, std::string_view(cookie, cookieLength), Now()); } int APS5_VABI sceHttp2GetCookie(int cookieBoxId, const char* url, char* cookie, std::size_t* required, std::size_t prepared) { std::lock_guard lock(g_cookiesMutex); CookieBox* box = FindBox(cookieBoxId); if (!box) return ERROR_INVALID_ID; if (!url) return ERROR_INVALID_VALUE; Url parsed{}; if (const int result = ParseUrl(url, parsed); result == ERROR_REJECTED) return ERROR_NOT_FOUND; else if (result != 0) return result; RemoveExpired(*box, Now()); std::vector matches; for (const Cookie& stored : box->cookies) { if (DomainMatches(parsed.host, stored.domain) && PathMatches(parsed.path, stored.path) && (!stored.secure || parsed.secure)) matches.push_back(&stored); } if (matches.empty()) return ERROR_NOT_FOUND; if (!cookie && !required) return ERROR_INVALID_VALUE; std::stable_sort(matches.begin(), matches.end(), [](const Cookie* left, const Cookie* right) { return left->path.size() > right->path.size(); }); for (auto run = matches.begin(); run != matches.end();) { const auto end = std::find_if(run, matches.end(), [&](const Cookie* match) { return match->path.size() != (*run)->path.size(); }); std::rotate(run, run + 1, end); run = end; } std::string header; for (const Cookie* match : matches) AppendCookie(header, *match); const std::size_t size = header.size() + HEADER_SLACK; if (!cookie) { *required = size; return 0; } if (prepared < size) { if (prepared != 0) cookie[0] = '\0'; return ERROR_OUT_OF_SIZE; } header.append({'\0', ' ', '\0'}); std::memcpy(cookie, header.data(), size); if (required) *required = size; return 0; } int APS5_VABI sceHttp2GetCookieStats(int cookieBoxId, Http2CookieStats* stats) { std::lock_guard lock(g_cookiesMutex); if (!stats) return ERROR_INVALID_VALUE; CookieBox* box = FindBox(cookieBoxId); if (!box) return ERROR_INVALID_ID; RemoveExpired(*box, Now()); stats->currentInuseSize = InuseSize(*box); stats->currentInuseNum = static_cast(box->cookies.size()); stats->maxInuseSize = box->maxInuseSize; stats->maxInuseNum = box->maxInuseNum; stats->removedNum = box->removedNum; return 0; } int APS5_VABI sceHttp2SetCookieMaxNum(int cookieBoxId, std::uint32_t num) { std::lock_guard lock(g_cookiesMutex); CookieBox* box = FindBox(cookieBoxId); if (!box) return ERROR_INVALID_ID; box->maxNum = num; return 0; } int APS5_VABI sceHttp2SetCookieMaxNumPerDomain(int cookieBoxId, std::uint32_t num) { std::lock_guard lock(g_cookiesMutex); CookieBox* box = FindBox(cookieBoxId); if (!box) return ERROR_INVALID_ID; box->maxNumPerDomain = num; return 0; } int APS5_VABI sceHttp2SetCookieMaxSize(int cookieBoxId, std::uint32_t size) { std::lock_guard lock(g_cookiesMutex); CookieBox* box = FindBox(cookieBoxId); if (!box) return ERROR_INVALID_ID; box->maxSize = size; return 0; } }