mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-28 12:05:06 +00:00
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VcCKPwMu3oNWaS8AUph6MW
430 lines
17 KiB
C++
430 lines
17 KiB
C++
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include "prx/libSceHttp2/include/Http2.hpp"
|
|
#include "SceTypes.hpp"
|
|
#include "Cookies.hpp"
|
|
|
|
namespace {
|
|
|
|
constexpr int ERROR_NOT_FOUND = static_cast<int>(0x817B1025);
|
|
constexpr int ERROR_INVALID_ID = static_cast<int>(0x817B1100);
|
|
constexpr int ERROR_OUT_OF_SIZE = static_cast<int>(0x817B1104);
|
|
constexpr int ERROR_INVALID_VALUE = static_cast<int>(0x817B11FE);
|
|
constexpr int ERROR_REJECTED = static_cast<int>(0x817B1219);
|
|
constexpr int ERROR_INVALID_URL = static_cast<int>(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<std::int64_t>::max();
|
|
constexpr std::int64_t EARLIEST = std::numeric_limits<std::int64_t>::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<Cookie> 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<int, CookieBox> g_cookieBoxes;
|
|
|
|
std::int64_t Now() {
|
|
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
char Lower(char c) {
|
|
return c >= 'A' && c <= 'Z' ? static_cast<char>(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<unsigned char>(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<std::int64_t> 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<int>(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<std::int64_t> 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::size_t>(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<std::ptrdiff_t>(evicted));
|
|
box.removedNum += static_cast<std::uint32_t>(evicted);
|
|
}
|
|
}
|
|
|
|
void RecordUsage(CookieBox& box) {
|
|
box.maxInuseSize = std::max(box.maxInuseSize, InuseSize(box));
|
|
box.maxInuseNum = std::max(box.maxInuseNum, static_cast<std::uint32_t>(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<std::int64_t> maxAge;
|
|
std::optional<std::int64_t> expires;
|
|
std::string domain;
|
|
std::optional<std::string> 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<const Cookie*> 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<std::uint32_t>(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;
|
|
}
|
|
|
|
}
|