// Copyright 2018 yuzu emulator team // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include "common/assert.h" #include "common/common_funcs.h" #include "common/common_paths.h" #include "common/file_util.h" #include "common/hex_util.h" #include "common/logging/log.h" #include "common/string_util.h" #include "core/core.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/mode.h" #include "core/file_sys/nca_metadata.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs.h" #include "core/file_sys/system_archive/system_archive.h" #include "core/file_sys/vfs_types.h" #include "core/frontend/applets/general_frontend.h" #include "core/frontend/applets/web_browser.h" #include "core/hle/kernel/process.h" #include "core/hle/service/am/applets/web_browser.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/loader.h" namespace Service::AM::Applets { enum class WebArgTLVType : u16 { InitialURL = 0x1, ShopArgumentsURL = 0x2, ///< TODO(DarkLordZach): This is not the official name. CallbackURL = 0x3, CallbackableURL = 0x4, ApplicationID = 0x5, DocumentPath = 0x6, DocumentKind = 0x7, SystemDataID = 0x8, ShareStartPage = 0x9, Whitelist = 0xA, News = 0xB, UserID = 0xE, AlbumEntry0 = 0xF, ScreenShotEnabled = 0x10, EcClientCertEnabled = 0x11, Unk12 = 0x12, PlayReportEnabled = 0x13, Unk14 = 0x14, Unk15 = 0x15, BootDisplayKind = 0x17, BackgroundKind = 0x18, FooterEnabled = 0x19, PointerEnabled = 0x1A, LeftStickMode = 0x1B, KeyRepeatFrame1 = 0x1C, KeyRepeatFrame2 = 0x1D, BootAsMediaPlayerInv = 0x1E, DisplayUrlKind = 0x1F, BootAsMediaPlayer = 0x21, ShopJumpEnabled = 0x22, MediaAutoPlayEnabled = 0x23, LobbyParameter = 0x24, ApplicationAlbumEntry = 0x26, JsExtensionEnabled = 0x27, AdditionalCommentText = 0x28, TouchEnabledOnContents = 0x29, UserAgentAdditionalString = 0x2A, AdditionalMediaData0 = 0x2B, MediaPlayerAutoCloseEnabled = 0x2C, PageCacheEnabled = 0x2D, WebAudioEnabled = 0x2E, Unk2F = 0x2F, YouTubeVideoWhitelist = 0x31, FooterFixedKind = 0x32, PageFadeEnabled = 0x33, MediaCreatorApplicationRatingAge = 0x34, BootLoadingIconEnabled = 0x35, PageScrollIndicationEnabled = 0x36, MediaPlayerSpeedControlEnabled = 0x37, AlbumEntry1 = 0x38, AlbumEntry2 = 0x39, AlbumEntry3 = 0x3A, AdditionalMediaData1 = 0x3B, AdditionalMediaData2 = 0x3C, AdditionalMediaData3 = 0x3D, BootFooterButton = 0x3E, OverrideWebAudioVolume = 0x3F, OverrideMediaAudioVolume = 0x40, BootMode = 0x41, WebSessionEnabled = 0x42, }; enum class ShimKind : u32 { Shop = 1, Login = 2, Offline = 3, Share = 4, Web = 5, Wifi = 6, Lobby = 7, }; enum class ShopWebTarget { ApplicationInfo, AddOnContentList, SubscriptionList, ConsumableItemList, Home, Settings, }; namespace { constexpr std::size_t SHIM_KIND_COUNT = 0x8; struct WebArgHeader { u16 count; INSERT_PADDING_BYTES(2); ShimKind kind; }; static_assert(sizeof(WebArgHeader) == 0x8, "WebArgHeader has incorrect size."); struct WebArgTLV { WebArgTLVType type; u16 size; u32 offset; }; static_assert(sizeof(WebArgTLV) == 0x8, "WebArgTLV has incorrect size."); struct WebCommonReturnValue { u32 result_code; INSERT_PADDING_BYTES(0x4); std::array last_url; u64 last_url_size; }; static_assert(sizeof(WebCommonReturnValue) == 0x1010, "WebCommonReturnValue has incorrect size."); struct WebWifiPageArg { INSERT_PADDING_BYTES(4); std::array connection_test_url; std::array initial_url; std::array nifm_network_uuid; u32 nifm_requirement; }; static_assert(sizeof(WebWifiPageArg) == 0x518, "WebWifiPageArg has incorrect size."); struct WebWifiReturnValue { INSERT_PADDING_BYTES(4); u32 result; }; static_assert(sizeof(WebWifiReturnValue) == 0x8, "WebWifiReturnValue has incorrect size."); enum class OfflineWebSource : u32 { OfflineHtmlPage = 0x1, ApplicationLegalInformation = 0x2, SystemDataPage = 0x3, }; std::map> GetWebArguments(const std::vector& arg) { if (arg.size() < sizeof(WebArgHeader)) return {}; WebArgHeader header{}; std::memcpy(&header, arg.data(), sizeof(WebArgHeader)); std::map> out; u64 offset = sizeof(WebArgHeader); for (std::size_t i = 0; i < header.count; ++i) { if (arg.size() < (offset + sizeof(WebArgTLV))) return out; WebArgTLV tlv{}; std::memcpy(&tlv, arg.data() + offset, sizeof(WebArgTLV)); offset += sizeof(WebArgTLV); offset += tlv.offset; if (arg.size() < (offset + tlv.size)) return out; std::vector data(tlv.size); std::memcpy(data.data(), arg.data() + offset, tlv.size); offset += tlv.size; out.insert_or_assign(tlv.type, data); } return out; } FileSys::VirtualFile GetApplicationRomFS(u64 title_id, FileSys::ContentRecordType type) { const auto& installed{Core::System::GetInstance().GetContentProvider()}; const auto res = installed.GetEntry(title_id, type); if (res != nullptr) { return res->GetRomFS(); } if (type == FileSys::ContentRecordType::Data) { return FileSys::SystemArchive::SynthesizeSystemArchive(title_id); } return nullptr; } } // Anonymous namespace WebBrowser::WebBrowser(Core::Frontend::WebBrowserApplet& frontend, u64 current_process_title_id, Core::Frontend::ECommerceApplet* frontend_e_commerce) : frontend(frontend), frontend_e_commerce(frontend_e_commerce), current_process_title_id(current_process_title_id) {} WebBrowser::~WebBrowser() = default; void WebBrowser::Initialize() { Applet::Initialize(); complete = false; temporary_dir.clear(); filename.clear(); status = RESULT_SUCCESS; const auto web_arg_storage = broker.PopNormalDataToApplet(); ASSERT(web_arg_storage != nullptr); const auto& web_arg = web_arg_storage->GetData(); ASSERT(web_arg.size() >= 0x8); std::memcpy(&kind, web_arg.data() + 0x4, sizeof(ShimKind)); args = GetWebArguments(web_arg); InitializeInternal(); } bool WebBrowser::TransactionComplete() const { return complete; } ResultCode WebBrowser::GetStatus() const { return status; } void WebBrowser::ExecuteInteractive() { UNIMPLEMENTED_MSG("Unexpected interactive data recieved!"); } void WebBrowser::Execute() { if (complete) { return; } if (status != RESULT_SUCCESS) { complete = true; return; } ExecuteInternal(); } void WebBrowser::UnpackRomFS() { if (unpacked) return; ASSERT(offline_romfs != nullptr); const auto dir = FileSys::ExtractRomFS(offline_romfs, FileSys::RomFSExtractionType::SingleDiscard); const auto& vfs{Core::System::GetInstance().GetFilesystem()}; const auto temp_dir = vfs->CreateDirectory(temporary_dir, FileSys::Mode::ReadWrite); FileSys::VfsRawCopyD(dir, temp_dir); unpacked = true; } void WebBrowser::Finalize() { complete = true; WebCommonReturnValue out{}; out.result_code = 0; out.last_url_size = 0; std::vector data(sizeof(WebCommonReturnValue)); std::memcpy(data.data(), &out, sizeof(WebCommonReturnValue)); broker.PushNormalDataFromApplet(IStorage{data}); broker.SignalStateChanged(); if (!temporary_dir.empty() && FileUtil::IsDirectory(temporary_dir)) { FileUtil::DeleteDirRecursively(temporary_dir); } } void WebBrowser::InitializeInternal() { using WebAppletInitializer = void (WebBrowser::*)(); constexpr std::array functions{ nullptr, &WebBrowser::InitializeShop, nullptr, &WebBrowser::InitializeOffline, nullptr, nullptr, nullptr, nullptr, }; const auto index = static_cast(kind); if (index > functions.size() || functions[index] == nullptr) { LOG_ERROR(Service_AM, "Invalid shim_kind={:08X}", index); return; } const auto function = functions[index]; (this->*function)(); } void WebBrowser::ExecuteInternal() { using WebAppletExecutor = void (WebBrowser::*)(); constexpr std::array functions{ nullptr, &WebBrowser::ExecuteShop, nullptr, &WebBrowser::ExecuteOffline, nullptr, nullptr, nullptr, nullptr, }; const auto index = static_cast(kind); if (index > functions.size() || functions[index] == nullptr) { LOG_ERROR(Service_AM, "Invalid shim_kind={:08X}", index); return; } const auto function = functions[index]; (this->*function)(); } void WebBrowser::InitializeShop() { if (frontend_e_commerce == nullptr) { LOG_ERROR(Service_AM, "Missing ECommerce Applet frontend!"); status = ResultCode(-1); return; } const auto user_id_data = args.find(WebArgTLVType::UserID); user_id = std::nullopt; if (user_id_data != args.end()) { user_id = u128{}; std::memcpy(user_id->data(), user_id_data->second.data(), sizeof(u128)); } const auto url = args.find(WebArgTLVType::ShopArgumentsURL); if (url == args.end()) { LOG_ERROR(Service_AM, "Missing EShop Arguments URL for initialization!"); status = ResultCode(-1); return; } std::vector split_query; Common::SplitString(Common::StringFromFixedZeroTerminatedBuffer( reinterpret_cast(url->second.data()), url->second.size()), '?', split_query); // 2 -> Main URL '?' Query Parameters // Less is missing info, More is malformed if (split_query.size() != 2) { LOG_ERROR(Service_AM, "EShop Arguments has more than one question mark, malformed"); status = ResultCode(-1); return; } std::vector queries; Common::SplitString(split_query[1], '&', queries); const auto split_single_query = [](const std::string& in) -> std::pair { const auto index = in.find('='); if (index == std::string::npos || index == in.size() - 1) { return {in, ""}; } return {in.substr(0, index), in.substr(index + 1)}; }; std::transform(queries.begin(), queries.end(), std::inserter(shop_query, std::next(shop_query.begin())), split_single_query); const auto scene = shop_query.find("scene"); if (scene == shop_query.end()) { LOG_ERROR(Service_AM, "No scene parameter was passed via shop query!"); status = ResultCode(-1); return; } const std::map> target_map{ {"product_detail", ShopWebTarget::ApplicationInfo}, {"aocs", ShopWebTarget::AddOnContentList}, {"subscriptions", ShopWebTarget::SubscriptionList}, {"consumption", ShopWebTarget::ConsumableItemList}, {"settings", ShopWebTarget::Settings}, {"top", ShopWebTarget::Home}, }; const auto target = target_map.find(scene->second); if (target == target_map.end()) { LOG_ERROR(Service_AM, "Scene for shop query is invalid! (scene={})", scene->second); status = ResultCode(-1); return; } shop_web_target = target->second; const auto title_id_data = shop_query.find("dst_app_id"); if (title_id_data != shop_query.end()) { title_id = std::stoull(title_id_data->second, nullptr, 0x10); } const auto mode_data = shop_query.find("mode"); if (mode_data != shop_query.end()) { shop_full_display = mode_data->second == "full"; } } void WebBrowser::InitializeOffline() { if (args.find(WebArgTLVType::DocumentPath) == args.end() || args.find(WebArgTLVType::DocumentKind) == args.end() || args.find(WebArgTLVType::ApplicationID) == args.end()) { status = ResultCode(-1); LOG_ERROR(Service_AM, "Missing necessary parameters for initialization!"); } const auto url_data = args[WebArgTLVType::DocumentPath]; filename = Common::StringFromFixedZeroTerminatedBuffer( reinterpret_cast(url_data.data()), url_data.size()); OfflineWebSource source; ASSERT(args[WebArgTLVType::DocumentKind].size() >= 4); std::memcpy(&source, args[WebArgTLVType::DocumentKind].data(), sizeof(OfflineWebSource)); constexpr std::array WEB_SOURCE_NAMES{ "manual", "legal", "system", }; temporary_dir = FileUtil::SanitizePath(FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + "web_applet_" + WEB_SOURCE_NAMES[static_cast(source) - 1], FileUtil::DirectorySeparator::PlatformDefault); FileUtil::DeleteDirRecursively(temporary_dir); u64 title_id = 0; // 0 corresponds to current process ASSERT(args[WebArgTLVType::ApplicationID].size() >= 0x8); std::memcpy(&title_id, args[WebArgTLVType::ApplicationID].data(), sizeof(u64)); FileSys::ContentRecordType type = FileSys::ContentRecordType::Data; switch (source) { case OfflineWebSource::OfflineHtmlPage: // While there is an AppID TLV field, in official SW this is always ignored. title_id = 0; type = FileSys::ContentRecordType::HtmlDocument; break; case OfflineWebSource::ApplicationLegalInformation: type = FileSys::ContentRecordType::LegalInformation; break; case OfflineWebSource::SystemDataPage: type = FileSys::ContentRecordType::Data; break; } if (title_id == 0) { title_id = current_process_title_id; } offline_romfs = GetApplicationRomFS(title_id, type); if (offline_romfs == nullptr) { status = ResultCode(-1); LOG_ERROR(Service_AM, "Failed to find offline data for request!"); } std::string path_additional_directory; if (source == OfflineWebSource::OfflineHtmlPage) { path_additional_directory = std::string(DIR_SEP).append("html-document"); } filename = FileUtil::SanitizePath(temporary_dir + path_additional_directory + DIR_SEP + filename, FileUtil::DirectorySeparator::PlatformDefault); } void WebBrowser::ExecuteShop() { const auto callback = [this]() { Finalize(); }; const auto check_optional_parameter = [this](const auto& p) { if (!p.has_value()) { LOG_ERROR(Service_AM, "Missing one or more necessary parameters for execution!"); status = ResultCode(-1); return false; } return true; }; switch (shop_web_target) { case ShopWebTarget::ApplicationInfo: if (!check_optional_parameter(title_id)) return; frontend_e_commerce->ShowApplicationInformation(callback, *title_id, user_id, shop_full_display, shop_extra_parameter); break; case ShopWebTarget::AddOnContentList: if (!check_optional_parameter(title_id)) return; frontend_e_commerce->ShowAddOnContentList(callback, *title_id, user_id, shop_full_display); break; case ShopWebTarget::ConsumableItemList: if (!check_optional_parameter(title_id)) return; frontend_e_commerce->ShowConsumableItemList(callback, *title_id, user_id); break; case ShopWebTarget::Home: if (!check_optional_parameter(user_id)) return; if (!check_optional_parameter(shop_full_display)) return; frontend_e_commerce->ShowShopHome(callback, *user_id, *shop_full_display); break; case ShopWebTarget::Settings: if (!check_optional_parameter(user_id)) return; if (!check_optional_parameter(shop_full_display)) return; frontend_e_commerce->ShowSettings(callback, *user_id, *shop_full_display); break; case ShopWebTarget::SubscriptionList: if (!check_optional_parameter(title_id)) return; frontend_e_commerce->ShowSubscriptionList(callback, *title_id, user_id); break; default: UNREACHABLE(); } } void WebBrowser::ExecuteOffline() { frontend.OpenPageLocal(filename, [this] { UnpackRomFS(); }, [this] { Finalize(); }); } } // namespace Service::AM::Applets