|
|
@ -25,13 +25,16 @@ constexpr char BOXCAT_HOSTNAME[] = "api.yuzu-emu.org"; |
|
|
|
|
|
|
|
|
// Formatted using fmt with arg[0] = hex title id
|
|
|
// Formatted using fmt with arg[0] = hex title id
|
|
|
constexpr char BOXCAT_PATHNAME_DATA[] = "/boxcat/titles/{:016X}/data"; |
|
|
constexpr char BOXCAT_PATHNAME_DATA[] = "/boxcat/titles/{:016X}/data"; |
|
|
|
|
|
constexpr char BOXCAT_PATHNAME_LAUNCHPARAM[] = "/boxcat/titles/{:016X}/launchparam"; |
|
|
|
|
|
|
|
|
constexpr char BOXCAT_PATHNAME_EVENTS[] = "/boxcat/events"; |
|
|
constexpr char BOXCAT_PATHNAME_EVENTS[] = "/boxcat/events"; |
|
|
|
|
|
|
|
|
constexpr char BOXCAT_API_VERSION[] = "1"; |
|
|
constexpr char BOXCAT_API_VERSION[] = "1"; |
|
|
|
|
|
constexpr char BOXCAT_CLIENT_TYPE[] = "yuzu"; |
|
|
|
|
|
|
|
|
// HTTP status codes for Boxcat
|
|
|
// HTTP status codes for Boxcat
|
|
|
enum class ResponseStatus { |
|
|
enum class ResponseStatus { |
|
|
|
|
|
Ok = 200, ///< Operation completed successfully.
|
|
|
BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server.
|
|
|
BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server.
|
|
|
NoUpdate = 304, ///< The digest provided would match the new data, no need to update.
|
|
|
NoUpdate = 304, ///< The digest provided would match the new data, no need to update.
|
|
|
NoMatchTitleId = 404, ///< The title ID provided doesn't have a boxcat implementation.
|
|
|
NoMatchTitleId = 404, ///< The title ID provided doesn't have a boxcat implementation.
|
|
|
@ -74,6 +77,11 @@ constexpr u64 VFS_COPY_BLOCK_SIZE = 1ull << 24; // 4MB |
|
|
|
|
|
|
|
|
namespace { |
|
|
namespace { |
|
|
|
|
|
|
|
|
|
|
|
std::string GetBINFilePath(u64 title_id) { |
|
|
|
|
|
return fmt::format("{}bcat/{:016X}/launchparam.bin", |
|
|
|
|
|
FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
std::string GetZIPFilePath(u64 title_id) { |
|
|
std::string GetZIPFilePath(u64 title_id) { |
|
|
return fmt::format("{}bcat/{:016X}/data.zip", |
|
|
return fmt::format("{}bcat/{:016X}/data.zip", |
|
|
FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); |
|
|
FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); |
|
|
@ -98,27 +106,40 @@ void HandleDownloadDisplayResult(DownloadResult res) { |
|
|
|
|
|
|
|
|
class Boxcat::Client { |
|
|
class Boxcat::Client { |
|
|
public: |
|
|
public: |
|
|
Client(std::string zip_path, u64 title_id, u64 build_id) |
|
|
|
|
|
: zip_path(std::move(zip_path)), title_id(title_id), build_id(build_id) {} |
|
|
|
|
|
|
|
|
Client(std::string path, u64 title_id, u64 build_id) |
|
|
|
|
|
: path(std::move(path)), title_id(title_id), build_id(build_id) {} |
|
|
|
|
|
|
|
|
|
|
|
DownloadResult DownloadDataZip() { |
|
|
|
|
|
return DownloadInternal(fmt::format(BOXCAT_PATHNAME_DATA, title_id), TIMEOUT_SECONDS, |
|
|
|
|
|
"Boxcat-Data-Digest", "application/zip"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
DownloadResult DownloadLaunchParam() { |
|
|
|
|
|
return DownloadInternal(fmt::format(BOXCAT_PATHNAME_LAUNCHPARAM, title_id), |
|
|
|
|
|
TIMEOUT_SECONDS / 3, "Boxcat-LaunchParam-Digest", |
|
|
|
|
|
"application/octet-stream"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
DownloadResult Download() { |
|
|
|
|
|
const auto resolved_path = fmt::format(BOXCAT_PATHNAME_DATA, title_id); |
|
|
|
|
|
|
|
|
private: |
|
|
|
|
|
DownloadResult DownloadInternal(const std::string& resolved_path, u32 timeout_seconds, |
|
|
|
|
|
const std::string& digest_header_name, |
|
|
|
|
|
const std::string& content_type_name) { |
|
|
if (client == nullptr) { |
|
|
if (client == nullptr) { |
|
|
client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, TIMEOUT_SECONDS); |
|
|
|
|
|
|
|
|
client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, timeout_seconds); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
httplib::Headers headers{ |
|
|
httplib::Headers headers{ |
|
|
{std::string("Boxcat-Client-Version"), std::string(BOXCAT_API_VERSION)}, |
|
|
{std::string("Boxcat-Client-Version"), std::string(BOXCAT_API_VERSION)}, |
|
|
|
|
|
{std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)}, |
|
|
{std::string("Boxcat-Build-Id"), fmt::format("{:016X}", build_id)}, |
|
|
{std::string("Boxcat-Build-Id"), fmt::format("{:016X}", build_id)}, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
if (FileUtil::Exists(zip_path)) { |
|
|
|
|
|
FileUtil::IOFile file{zip_path, "rb"}; |
|
|
|
|
|
|
|
|
if (FileUtil::Exists(path)) { |
|
|
|
|
|
FileUtil::IOFile file{path, "rb"}; |
|
|
std::vector<u8> bytes(file.GetSize()); |
|
|
std::vector<u8> bytes(file.GetSize()); |
|
|
file.ReadBytes(bytes.data(), bytes.size()); |
|
|
file.ReadBytes(bytes.data(), bytes.size()); |
|
|
const auto digest = DigestFile(bytes); |
|
|
const auto digest = DigestFile(bytes); |
|
|
headers.insert({std::string("Boxcat-Current-Zip-Digest"), |
|
|
|
|
|
Common::HexArrayToString(digest, false)}); |
|
|
|
|
|
|
|
|
headers.insert({digest_header_name, Common::HexArrayToString(digest, false)}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const auto response = client->Get(resolved_path.c_str(), headers); |
|
|
const auto response = client->Get(resolved_path.c_str(), headers); |
|
|
@ -133,17 +154,17 @@ public: |
|
|
return DownloadResult::NoMatchTitleId; |
|
|
return DownloadResult::NoMatchTitleId; |
|
|
if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId)) |
|
|
if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId)) |
|
|
return DownloadResult::NoMatchBuildId; |
|
|
return DownloadResult::NoMatchBuildId; |
|
|
if (response->status >= 400) |
|
|
|
|
|
|
|
|
if (response->status != static_cast<int>(ResponseStatus::Ok)) |
|
|
return DownloadResult::GeneralWebError; |
|
|
return DownloadResult::GeneralWebError; |
|
|
|
|
|
|
|
|
const auto content_type = response->headers.find("content-type"); |
|
|
const auto content_type = response->headers.find("content-type"); |
|
|
if (content_type == response->headers.end() || |
|
|
if (content_type == response->headers.end() || |
|
|
content_type->second.find("application/zip") == std::string::npos) { |
|
|
|
|
|
|
|
|
content_type->second.find(content_type_name) == std::string::npos) { |
|
|
return DownloadResult::InvalidContentType; |
|
|
return DownloadResult::InvalidContentType; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
FileUtil::CreateFullPath(zip_path); |
|
|
|
|
|
FileUtil::IOFile file{zip_path, "wb"}; |
|
|
|
|
|
|
|
|
FileUtil::CreateFullPath(path); |
|
|
|
|
|
FileUtil::IOFile file{path, "wb"}; |
|
|
if (!file.IsOpen()) |
|
|
if (!file.IsOpen()) |
|
|
return DownloadResult::GeneralFSError; |
|
|
return DownloadResult::GeneralFSError; |
|
|
if (!file.Resize(response->body.size())) |
|
|
if (!file.Resize(response->body.size())) |
|
|
@ -154,7 +175,6 @@ public: |
|
|
return DownloadResult::Success; |
|
|
return DownloadResult::Success; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private: |
|
|
|
|
|
using Digest = std::array<u8, 0x20>; |
|
|
using Digest = std::array<u8, 0x20>; |
|
|
static Digest DigestFile(std::vector<u8> bytes) { |
|
|
static Digest DigestFile(std::vector<u8> bytes) { |
|
|
Digest out{}; |
|
|
Digest out{}; |
|
|
@ -163,7 +183,7 @@ private: |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
std::unique_ptr<httplib::Client> client; |
|
|
std::unique_ptr<httplib::Client> client; |
|
|
std::string zip_path; |
|
|
|
|
|
|
|
|
std::string path; |
|
|
u64 title_id; |
|
|
u64 title_id; |
|
|
u64 build_id; |
|
|
u64 build_id; |
|
|
}; |
|
|
}; |
|
|
@ -191,9 +211,14 @@ void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title, |
|
|
const auto zip_path{GetZIPFilePath(title.title_id)}; |
|
|
const auto zip_path{GetZIPFilePath(title.title_id)}; |
|
|
Boxcat::Client client{zip_path, title.title_id, title.build_id}; |
|
|
Boxcat::Client client{zip_path, title.title_id, title.build_id}; |
|
|
|
|
|
|
|
|
const auto res = client.Download(); |
|
|
|
|
|
|
|
|
const auto res = client.DownloadDataZip(); |
|
|
if (res != DownloadResult::Success) { |
|
|
if (res != DownloadResult::Success) { |
|
|
LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); |
|
|
LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); |
|
|
|
|
|
|
|
|
|
|
|
if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) { |
|
|
|
|
|
FileUtil::Delete(zip_path); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
HandleDownloadDisplayResult(res); |
|
|
HandleDownloadDisplayResult(res); |
|
|
failure(); |
|
|
failure(); |
|
|
return; |
|
|
return; |
|
|
@ -286,6 +311,39 @@ void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) { |
|
|
Common::HexArrayToString(passphrase)); |
|
|
Common::HexArrayToString(passphrase)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title) { |
|
|
|
|
|
const auto path{GetBINFilePath(title.title_id)}; |
|
|
|
|
|
|
|
|
|
|
|
if (Settings::values.bcat_boxcat_local) { |
|
|
|
|
|
LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download."); |
|
|
|
|
|
} else { |
|
|
|
|
|
Boxcat::Client client{path, title.title_id, title.build_id}; |
|
|
|
|
|
|
|
|
|
|
|
const auto res = client.DownloadLaunchParam(); |
|
|
|
|
|
if (res != DownloadResult::Success) { |
|
|
|
|
|
LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); |
|
|
|
|
|
|
|
|
|
|
|
if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) { |
|
|
|
|
|
FileUtil::Delete(path); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
HandleDownloadDisplayResult(res); |
|
|
|
|
|
return std::nullopt; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
FileUtil::IOFile bin{path, "rb"}; |
|
|
|
|
|
const auto size = bin.GetSize(); |
|
|
|
|
|
std::vector<u8> bytes(size); |
|
|
|
|
|
if (size == 0 || bin.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) { |
|
|
|
|
|
LOG_ERROR(Service_BCAT, "Boxcat failed to read launch parameter binary at path '{}'!", |
|
|
|
|
|
path); |
|
|
|
|
|
return std::nullopt; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return bytes; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global, |
|
|
Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global, |
|
|
std::map<std::string, EventStatus>& games) { |
|
|
std::map<std::string, EventStatus>& games) { |
|
|
httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT), |
|
|
httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT), |
|
|
@ -293,6 +351,7 @@ Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global, |
|
|
|
|
|
|
|
|
httplib::Headers headers{ |
|
|
httplib::Headers headers{ |
|
|
{std::string("Boxcat-Client-Version"), std::string(BOXCAT_API_VERSION)}, |
|
|
{std::string("Boxcat-Client-Version"), std::string(BOXCAT_API_VERSION)}, |
|
|
|
|
|
{std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)}, |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers); |
|
|
const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers); |
|
|
|