diff options
| author | 2019-04-28 18:47:58 -0400 | |
|---|---|---|
| committer | 2019-09-30 17:21:53 -0400 | |
| commit | 2903f3524e7b9d802da4d23ae6d25d07f7eba8f5 (patch) | |
| tree | 440b572006280fcee61775222d2856c0af1504c7 /src | |
| parent | bcat: Add backend class to generify the functions of BCAT (diff) | |
| download | yuzu-2903f3524e7b9d802da4d23ae6d25d07f7eba8f5.tar.gz yuzu-2903f3524e7b9d802da4d23ae6d25d07f7eba8f5.tar.xz yuzu-2903f3524e7b9d802da4d23ae6d25d07f7eba8f5.zip | |
bcat: Add BCAT backend for Boxcat service
Downloads content from yuzu servers and unpacks it into the temporary directory provided. Fully supports all Backend features except passphrase.
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/hle/service/bcat/backend/boxcat.cpp | 351 | ||||
| -rw-r--r-- | src/core/hle/service/bcat/backend/boxcat.h | 56 |
2 files changed, 407 insertions, 0 deletions
diff --git a/src/core/hle/service/bcat/backend/boxcat.cpp b/src/core/hle/service/bcat/backend/boxcat.cpp new file mode 100644 index 000000000..539140f30 --- /dev/null +++ b/src/core/hle/service/bcat/backend/boxcat.cpp | |||
| @@ -0,0 +1,351 @@ | |||
| 1 | // Copyright 2019 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <fmt/ostream.h> | ||
| 6 | #include <httplib.h> | ||
| 7 | #include <json.hpp> | ||
| 8 | #include <mbedtls/sha256.h> | ||
| 9 | #include "common/hex_util.h" | ||
| 10 | #include "common/logging/backend.h" | ||
| 11 | #include "common/logging/log.h" | ||
| 12 | #include "core/core.h" | ||
| 13 | #include "core/file_sys/vfs.h" | ||
| 14 | #include "core/file_sys/vfs_libzip.h" | ||
| 15 | #include "core/file_sys/vfs_vector.h" | ||
| 16 | #include "core/frontend/applets/error.h" | ||
| 17 | #include "core/hle/lock.h" | ||
| 18 | #include "core/hle/service/am/applets/applets.h" | ||
| 19 | #include "core/hle/service/bcat/backend/boxcat.h" | ||
| 20 | #include "core/settings.h" | ||
| 21 | |||
| 22 | namespace Service::BCAT { | ||
| 23 | |||
| 24 | constexpr char BOXCAT_HOSTNAME[] = "api.yuzu-emu.org"; | ||
| 25 | |||
| 26 | // Formatted using fmt with arg[0] = hex title id | ||
| 27 | constexpr char BOXCAT_PATHNAME_DATA[] = "/boxcat/titles/{:016X}/data"; | ||
| 28 | |||
| 29 | constexpr char BOXCAT_PATHNAME_EVENTS[] = "/boxcat/events"; | ||
| 30 | |||
| 31 | constexpr char BOXCAT_API_VERSION[] = "1"; | ||
| 32 | |||
| 33 | // HTTP status codes for Boxcat | ||
| 34 | enum class ResponseStatus { | ||
| 35 | BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server. | ||
| 36 | NoUpdate = 304, ///< The digest provided would match the new data, no need to update. | ||
| 37 | NoMatchTitleId = 404, ///< The title ID provided doesn't have a boxcat implementation. | ||
| 38 | NoMatchBuildId = 406, ///< The build ID provided is blacklisted (potentially because of format | ||
| 39 | ///< issues or whatnot) and has no data. | ||
| 40 | }; | ||
| 41 | |||
| 42 | enum class DownloadResult { | ||
| 43 | Success = 0, | ||
| 44 | NoResponse, | ||
| 45 | GeneralWebError, | ||
| 46 | NoMatchTitleId, | ||
| 47 | NoMatchBuildId, | ||
| 48 | InvalidContentType, | ||
| 49 | GeneralFSError, | ||
| 50 | BadClientVersion, | ||
| 51 | }; | ||
| 52 | |||
| 53 | constexpr std::array<const char*, 8> DOWNLOAD_RESULT_LOG_MESSAGES{ | ||
| 54 | "Success", | ||
| 55 | "There was no response from the server.", | ||
| 56 | "There was a general web error code returned from the server.", | ||
| 57 | "The title ID of the current game doesn't have a boxcat implementation. If you believe an " | ||
| 58 | "implementation should be added, contact yuzu support.", | ||
| 59 | "The build ID of the current version of the game is marked as incompatible with the current " | ||
| 60 | "BCAT distribution. Try upgrading or downgrading your game version or contacting yuzu support.", | ||
| 61 | "The content type of the web response was invalid.", | ||
| 62 | "There was a general filesystem error while saving the zip file.", | ||
| 63 | "The server is either too new or too old to serve the request. Try using the latest version of " | ||
| 64 | "an official release of yuzu.", | ||
| 65 | }; | ||
| 66 | |||
| 67 | std::ostream& operator<<(std::ostream& os, DownloadResult result) { | ||
| 68 | return os << DOWNLOAD_RESULT_LOG_MESSAGES.at(static_cast<std::size_t>(result)); | ||
| 69 | } | ||
| 70 | |||
| 71 | constexpr u32 PORT = 443; | ||
| 72 | constexpr u32 TIMEOUT_SECONDS = 30; | ||
| 73 | constexpr u64 VFS_COPY_BLOCK_SIZE = 1ull << 24; // 4MB | ||
| 74 | |||
| 75 | namespace { | ||
| 76 | |||
| 77 | std::string GetZIPFilePath(u64 title_id) { | ||
| 78 | return fmt::format("{}bcat/{:016X}/data.zip", | ||
| 79 | FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); | ||
| 80 | } | ||
| 81 | |||
| 82 | // If the error is something the user should know about (build ID mismatch, bad client version), | ||
| 83 | // display an error. | ||
| 84 | void HandleDownloadDisplayResult(DownloadResult res) { | ||
| 85 | if (res == DownloadResult::Success || res == DownloadResult::NoResponse || | ||
| 86 | res == DownloadResult::GeneralWebError || res == DownloadResult::GeneralFSError || | ||
| 87 | res == DownloadResult::NoMatchTitleId || res == DownloadResult::InvalidContentType) { | ||
| 88 | return; | ||
| 89 | } | ||
| 90 | |||
| 91 | const auto& frontend{Core::System::GetInstance().GetAppletManager().GetAppletFrontendSet()}; | ||
| 92 | frontend.error->ShowCustomErrorText( | ||
| 93 | ResultCode(-1), "There was an error while attempting to use Boxcat.", | ||
| 94 | DOWNLOAD_RESULT_LOG_MESSAGES[static_cast<std::size_t>(res)], [] {}); | ||
| 95 | } | ||
| 96 | |||
| 97 | } // namespace | ||
| 98 | |||
| 99 | class Boxcat::Client { | ||
| 100 | public: | ||
| 101 | Client(std::string zip_path, u64 title_id, u64 build_id) | ||
| 102 | : zip_path(std::move(zip_path)), title_id(title_id), build_id(build_id) {} | ||
| 103 | |||
| 104 | DownloadResult Download() { | ||
| 105 | const auto resolved_path = fmt::format(BOXCAT_PATHNAME_DATA, title_id); | ||
| 106 | if (client == nullptr) { | ||
| 107 | client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, TIMEOUT_SECONDS); | ||
| 108 | } | ||
| 109 | |||
| 110 | httplib::Headers headers{ | ||
| 111 | {std::string("Boxcat-Client-Version"), std::string(BOXCAT_API_VERSION)}, | ||
| 112 | {std::string("Boxcat-Build-Id"), fmt::format("{:016X}", build_id)}, | ||
| 113 | }; | ||
| 114 | |||
| 115 | if (FileUtil::Exists(zip_path)) { | ||
| 116 | FileUtil::IOFile file{zip_path, "rb"}; | ||
| 117 | std::vector<u8> bytes(file.GetSize()); | ||
| 118 | file.ReadBytes(bytes.data(), bytes.size()); | ||
| 119 | const auto digest = DigestFile(bytes); | ||
| 120 | headers.insert({std::string("Boxcat-Current-Zip-Digest"), | ||
| 121 | Common::HexArrayToString(digest, false)}); | ||
| 122 | } | ||
| 123 | |||
| 124 | const auto response = client->Get(resolved_path.c_str(), headers); | ||
| 125 | if (response == nullptr) | ||
| 126 | return DownloadResult::NoResponse; | ||
| 127 | |||
| 128 | if (response->status == static_cast<int>(ResponseStatus::NoUpdate)) | ||
| 129 | return DownloadResult::Success; | ||
| 130 | if (response->status == static_cast<int>(ResponseStatus::BadClientVersion)) | ||
| 131 | return DownloadResult::BadClientVersion; | ||
| 132 | if (response->status == static_cast<int>(ResponseStatus::NoMatchTitleId)) | ||
| 133 | return DownloadResult::NoMatchTitleId; | ||
| 134 | if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId)) | ||
| 135 | return DownloadResult::NoMatchBuildId; | ||
| 136 | if (response->status >= 400) | ||
| 137 | return DownloadResult::GeneralWebError; | ||
| 138 | |||
| 139 | const auto content_type = response->headers.find("content-type"); | ||
| 140 | if (content_type == response->headers.end() || | ||
| 141 | content_type->second.find("application/zip") == std::string::npos) { | ||
| 142 | return DownloadResult::InvalidContentType; | ||
| 143 | } | ||
| 144 | |||
| 145 | FileUtil::CreateFullPath(zip_path); | ||
| 146 | FileUtil::IOFile file{zip_path, "wb"}; | ||
| 147 | if (!file.IsOpen()) | ||
| 148 | return DownloadResult::GeneralFSError; | ||
| 149 | if (!file.Resize(response->body.size())) | ||
| 150 | return DownloadResult::GeneralFSError; | ||
| 151 | if (file.WriteBytes(response->body.data(), response->body.size()) != response->body.size()) | ||
| 152 | return DownloadResult::GeneralFSError; | ||
| 153 | |||
| 154 | return DownloadResult::Success; | ||
| 155 | } | ||
| 156 | |||
| 157 | private: | ||
| 158 | using Digest = std::array<u8, 0x20>; | ||
| 159 | static Digest DigestFile(std::vector<u8> bytes) { | ||
| 160 | Digest out{}; | ||
| 161 | mbedtls_sha256(bytes.data(), bytes.size(), out.data(), 0); | ||
| 162 | return out; | ||
| 163 | } | ||
| 164 | |||
| 165 | std::unique_ptr<httplib::Client> client; | ||
| 166 | std::string zip_path; | ||
| 167 | u64 title_id; | ||
| 168 | u64 build_id; | ||
| 169 | }; | ||
| 170 | |||
| 171 | Boxcat::Boxcat(DirectoryGetter getter) : Backend(std::move(getter)) {} | ||
| 172 | |||
| 173 | Boxcat::~Boxcat() = default; | ||
| 174 | |||
| 175 | void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title, | ||
| 176 | CompletionCallback callback, std::optional<std::string> dir_name = {}) { | ||
| 177 | const auto failure = [&callback] { | ||
| 178 | // Acquire the HLE mutex | ||
| 179 | std::lock_guard lock{HLE::g_hle_lock}; | ||
| 180 | callback(false); | ||
| 181 | }; | ||
| 182 | |||
| 183 | if (Settings::values.bcat_boxcat_local) { | ||
| 184 | LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download."); | ||
| 185 | // Acquire the HLE mutex | ||
| 186 | std::lock_guard lock{HLE::g_hle_lock}; | ||
| 187 | callback(true); | ||
| 188 | return; | ||
| 189 | } | ||
| 190 | |||
| 191 | const auto zip_path{GetZIPFilePath(title.title_id)}; | ||
| 192 | Boxcat::Client client{zip_path, title.title_id, title.build_id}; | ||
| 193 | |||
| 194 | const auto res = client.Download(); | ||
| 195 | if (res != DownloadResult::Success) { | ||
| 196 | LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); | ||
| 197 | HandleDownloadDisplayResult(res); | ||
| 198 | failure(); | ||
| 199 | return; | ||
| 200 | } | ||
| 201 | |||
| 202 | FileUtil::IOFile zip{zip_path, "rb"}; | ||
| 203 | const auto size = zip.GetSize(); | ||
| 204 | std::vector<u8> bytes(size); | ||
| 205 | if (size == 0 || zip.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) { | ||
| 206 | LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!", zip_path); | ||
| 207 | failure(); | ||
| 208 | return; | ||
| 209 | } | ||
| 210 | |||
| 211 | const auto extracted = FileSys::ExtractZIP(std::make_shared<FileSys::VectorVfsFile>(bytes)); | ||
| 212 | if (extracted == nullptr) { | ||
| 213 | LOG_ERROR(Service_BCAT, "Boxcat failed to extract ZIP file!"); | ||
| 214 | failure(); | ||
| 215 | return; | ||
| 216 | } | ||
| 217 | |||
| 218 | if (dir_name == std::nullopt) { | ||
| 219 | const auto target_dir = dir_getter(title.title_id); | ||
| 220 | if (target_dir == nullptr || | ||
| 221 | !FileSys::VfsRawCopyD(extracted, target_dir, VFS_COPY_BLOCK_SIZE)) { | ||
| 222 | LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!"); | ||
| 223 | failure(); | ||
| 224 | return; | ||
| 225 | } | ||
| 226 | } else { | ||
| 227 | const auto target_dir = dir_getter(title.title_id); | ||
| 228 | if (target_dir == nullptr) { | ||
| 229 | LOG_ERROR(Service_BCAT, "Boxcat failed to get directory for title ID!"); | ||
| 230 | failure(); | ||
| 231 | return; | ||
| 232 | } | ||
| 233 | |||
| 234 | const auto target_sub = target_dir->GetSubdirectory(*dir_name); | ||
| 235 | const auto source_sub = extracted->GetSubdirectory(*dir_name); | ||
| 236 | |||
| 237 | if (target_sub == nullptr || source_sub == nullptr || | ||
| 238 | !FileSys::VfsRawCopyD(source_sub, target_sub, VFS_COPY_BLOCK_SIZE)) { | ||
| 239 | LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!"); | ||
| 240 | failure(); | ||
| 241 | return; | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | // Acquire the HLE mutex | ||
| 246 | std::lock_guard lock{HLE::g_hle_lock}; | ||
| 247 | callback(true); | ||
| 248 | } | ||
| 249 | |||
| 250 | bool Boxcat::Synchronize(TitleIDVersion title, CompletionCallback callback) { | ||
| 251 | is_syncing.exchange(true); | ||
| 252 | std::thread(&SynchronizeInternal, dir_getter, title, callback, std::nullopt).detach(); | ||
| 253 | return true; | ||
| 254 | } | ||
| 255 | |||
| 256 | bool Boxcat::SynchronizeDirectory(TitleIDVersion title, std::string name, | ||
| 257 | CompletionCallback callback) { | ||
| 258 | is_syncing.exchange(true); | ||
| 259 | std::thread(&SynchronizeInternal, dir_getter, title, callback, name).detach(); | ||
| 260 | return true; | ||
| 261 | } | ||
| 262 | |||
| 263 | bool Boxcat::Clear(u64 title_id) { | ||
| 264 | if (Settings::values.bcat_boxcat_local) { | ||
| 265 | LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping clear."); | ||
| 266 | return true; | ||
| 267 | } | ||
| 268 | |||
| 269 | const auto dir = dir_getter(title_id); | ||
| 270 | |||
| 271 | std::vector<std::string> dirnames; | ||
| 272 | |||
| 273 | for (const auto& subdir : dir->GetSubdirectories()) | ||
| 274 | dirnames.push_back(subdir->GetName()); | ||
| 275 | |||
| 276 | for (const auto& subdir : dirnames) { | ||
| 277 | if (!dir->DeleteSubdirectoryRecursive(subdir)) | ||
| 278 | return false; | ||
| 279 | } | ||
| 280 | |||
| 281 | return true; | ||
| 282 | } | ||
| 283 | |||
| 284 | void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) { | ||
| 285 | LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id, | ||
| 286 | Common::HexArrayToString(passphrase)); | ||
| 287 | } | ||
| 288 | |||
| 289 | Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global, | ||
| 290 | std::map<std::string, EventStatus>& games) { | ||
| 291 | httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT), | ||
| 292 | static_cast<int>(TIMEOUT_SECONDS)}; | ||
| 293 | |||
| 294 | httplib::Headers headers{ | ||
| 295 | {std::string("Boxcat-Client-Version"), std::string(BOXCAT_API_VERSION)}, | ||
| 296 | }; | ||
| 297 | |||
| 298 | const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers); | ||
| 299 | if (response == nullptr) | ||
| 300 | return StatusResult::Offline; | ||
| 301 | |||
| 302 | if (response->status == static_cast<int>(ResponseStatus::BadClientVersion)) | ||
| 303 | return StatusResult::BadClientVersion; | ||
| 304 | |||
| 305 | try { | ||
| 306 | nlohmann::json json = nlohmann::json::parse(response->body); | ||
| 307 | |||
| 308 | if (!json["online"].get<bool>()) | ||
| 309 | return StatusResult::Offline; | ||
| 310 | |||
| 311 | if (json["global"].is_null()) | ||
| 312 | global = std::nullopt; | ||
| 313 | else | ||
| 314 | global = json["global"].get<std::string>(); | ||
| 315 | |||
| 316 | if (json["games"].is_array()) { | ||
| 317 | for (const auto object : json["games"]) { | ||
| 318 | if (object.is_object() && object.find("name") != object.end()) { | ||
| 319 | EventStatus detail{}; | ||
| 320 | if (object["header"].is_string()) { | ||
| 321 | detail.header = object["header"].get<std::string>(); | ||
| 322 | } else { | ||
| 323 | detail.header = std::nullopt; | ||
| 324 | } | ||
| 325 | |||
| 326 | if (object["footer"].is_string()) { | ||
| 327 | detail.footer = object["footer"].get<std::string>(); | ||
| 328 | } else { | ||
| 329 | detail.footer = std::nullopt; | ||
| 330 | } | ||
| 331 | |||
| 332 | if (object["events"].is_array()) { | ||
| 333 | for (const auto& event : object["events"]) { | ||
| 334 | if (!event.is_string()) | ||
| 335 | continue; | ||
| 336 | detail.events.push_back(event.get<std::string>()); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | |||
| 340 | games.insert_or_assign(object["name"], std::move(detail)); | ||
| 341 | } | ||
| 342 | } | ||
| 343 | } | ||
| 344 | |||
| 345 | return StatusResult::Success; | ||
| 346 | } catch (const nlohmann::json::parse_error& e) { | ||
| 347 | return StatusResult::ParseError; | ||
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 | } // namespace Service::BCAT | ||
diff --git a/src/core/hle/service/bcat/backend/boxcat.h b/src/core/hle/service/bcat/backend/boxcat.h new file mode 100644 index 000000000..f4e60f264 --- /dev/null +++ b/src/core/hle/service/bcat/backend/boxcat.h | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | // Copyright 2019 yuzu emulator team | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <atomic> | ||
| 8 | #include <map> | ||
| 9 | #include <optional> | ||
| 10 | #include "core/hle/service/bcat/backend/backend.h" | ||
| 11 | |||
| 12 | namespace Service::BCAT { | ||
| 13 | |||
| 14 | struct EventStatus { | ||
| 15 | std::optional<std::string> header; | ||
| 16 | std::optional<std::string> footer; | ||
| 17 | std::vector<std::string> events; | ||
| 18 | }; | ||
| 19 | |||
| 20 | /// Boxcat is yuzu's custom backend implementation of Nintendo's BCAT service. It is free to use and | ||
| 21 | /// doesn't require a switch or nintendo account. The content is controlled by the yuzu team. | ||
| 22 | class Boxcat final : public Backend { | ||
| 23 | friend void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title, | ||
| 24 | CompletionCallback callback, | ||
| 25 | std::optional<std::string> dir_name); | ||
| 26 | |||
| 27 | public: | ||
| 28 | explicit Boxcat(DirectoryGetter getter); | ||
| 29 | ~Boxcat() override; | ||
| 30 | |||
| 31 | bool Synchronize(TitleIDVersion title, CompletionCallback callback) override; | ||
| 32 | bool SynchronizeDirectory(TitleIDVersion title, std::string name, | ||
| 33 | CompletionCallback callback) override; | ||
| 34 | |||
| 35 | bool Clear(u64 title_id) override; | ||
| 36 | |||
| 37 | void SetPassphrase(u64 title_id, const Passphrase& passphrase) override; | ||
| 38 | |||
| 39 | enum class StatusResult { | ||
| 40 | Success, | ||
| 41 | Offline, | ||
| 42 | ParseError, | ||
| 43 | BadClientVersion, | ||
| 44 | }; | ||
| 45 | |||
| 46 | static StatusResult GetStatus(std::optional<std::string>& global, | ||
| 47 | std::map<std::string, EventStatus>& games); | ||
| 48 | |||
| 49 | private: | ||
| 50 | std::atomic_bool is_syncing{false}; | ||
| 51 | |||
| 52 | class Client; | ||
| 53 | std::unique_ptr<Client> client; | ||
| 54 | }; | ||
| 55 | |||
| 56 | } // namespace Service::BCAT | ||