From 2154941c2c30e6a728c4212a17d492da439e922e Mon Sep 17 00:00:00 2001 From: Sketch <75850871+SketchMaster2001@users.noreply.github.com> Date: Mon, 14 Aug 2023 21:02:57 -0400 Subject: [PATCH] IOS/KD: Implement NWC24_CHECK_MAIL_NOW --- Source/Core/Common/CMakeLists.txt | 2 + Source/Core/Common/Crypto/HMAC.cpp | 27 ++ Source/Core/Common/Crypto/HMAC.h | 14 ++ Source/Core/Common/HttpRequest.cpp | 37 +++ Source/Core/Common/HttpRequest.h | 1 + Source/Core/Common/StringUtil.cpp | 5 + Source/Core/Common/StringUtil.h | 2 + Source/Core/Core/CMakeLists.txt | 3 + .../Core/IOS/Network/KD/Mail/MailCommon.h | 74 ++++++ .../Core/IOS/Network/KD/Mail/WC24Send.cpp | 68 +++++ .../Core/Core/IOS/Network/KD/Mail/WC24Send.h | 52 ++++ .../Core/Core/IOS/Network/KD/NWC24Config.cpp | 12 + Source/Core/Core/IOS/Network/KD/NWC24Config.h | 6 +- .../Core/Core/IOS/Network/KD/NetKDRequest.cpp | 237 ++++++++++++++++-- .../Core/Core/IOS/Network/KD/NetKDRequest.h | 42 +++- Source/Core/DolphinLib.props | 5 + 16 files changed, 569 insertions(+), 18 deletions(-) create mode 100644 Source/Core/Common/Crypto/HMAC.cpp create mode 100644 Source/Core/Common/Crypto/HMAC.h create mode 100644 Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h create mode 100644 Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp create mode 100644 Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index 5f3194e09d..9288f1556c 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -29,6 +29,8 @@ add_library(common Crypto/bn.h Crypto/ec.cpp Crypto/ec.h + Crypto/HMAC.cpp + Crypto/HMAC.h Crypto/SHA1.cpp Crypto/SHA1.h Debug/MemoryPatches.cpp diff --git a/Source/Core/Common/Crypto/HMAC.cpp b/Source/Core/Common/Crypto/HMAC.cpp new file mode 100644 index 0000000000..660817ea75 --- /dev/null +++ b/Source/Core/Common/Crypto/HMAC.cpp @@ -0,0 +1,27 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "Common/Crypto/HMAC.h" +#include "Common/ScopeGuard.h" + +namespace Common::HMAC +{ +bool HMACWithSHA1(std::span key, std::span msg, u8* out) +{ + mbedtls_md_context_t ctx; + Common::ScopeGuard guard{[&ctx] { mbedtls_md_free(&ctx); }}; + mbedtls_md_init(&ctx); + if (mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA1), 1)) + return false; + + if (mbedtls_md_hmac_starts(&ctx, key.data(), key.size()) || + mbedtls_md_hmac_update(&ctx, msg.data(), msg.size()) || mbedtls_md_hmac_finish(&ctx, out)) + { + return false; + } + + return true; +} +} // namespace Common::HMAC diff --git a/Source/Core/Common/Crypto/HMAC.h b/Source/Core/Common/Crypto/HMAC.h new file mode 100644 index 0000000000..e870a2c98e --- /dev/null +++ b/Source/Core/Common/Crypto/HMAC.h @@ -0,0 +1,14 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Common/CommonTypes.h" + +#include + +namespace Common::HMAC +{ +// HMAC with the SHA1 message digest. Excepted output length is 20 bytes. +bool HMACWithSHA1(std::span key, std::span msg, u8* out); +} // namespace Common::HMAC diff --git a/Source/Core/Common/HttpRequest.cpp b/Source/Core/Common/HttpRequest.cpp index b3dfb0dc0c..3dc0f1d328 100644 --- a/Source/Core/Common/HttpRequest.cpp +++ b/Source/Core/Common/HttpRequest.cpp @@ -27,6 +27,7 @@ public: explicit Impl(std::chrono::milliseconds timeout_ms, ProgressCallback callback); bool IsValid() const; + std::string GetHeaderValue(std::string_view name) const; void SetCookies(const std::string& cookies); void UseIPv4(); void FollowRedirects(long max); @@ -41,6 +42,7 @@ public: private: static inline std::once_flag s_curl_was_initialized; ProgressCallback m_callback; + Headers m_response_headers; std::unique_ptr m_curl{nullptr, curl_easy_cleanup}; std::string m_error_string; }; @@ -82,6 +84,11 @@ s32 HttpRequest::GetLastResponseCode() const return m_impl->GetLastResponseCode(); } +std::string HttpRequest::GetHeaderValue(std::string_view name) const +{ + return m_impl->GetHeaderValue(name); +} + HttpRequest::Response HttpRequest::Get(const std::string& url, const Headers& headers, AllowedReturnCodes codes) { @@ -173,6 +180,17 @@ void HttpRequest::Impl::FollowRedirects(long max) curl_easy_setopt(m_curl.get(), CURLOPT_MAXREDIRS, max); } +std::string HttpRequest::Impl::GetHeaderValue(std::string_view name) const +{ + for (const auto& [key, value] : m_response_headers) + { + if (key == name) + return value.value(); + } + + return {}; +} + std::string HttpRequest::Impl::EscapeComponent(const std::string& string) { char* escaped = curl_easy_escape(m_curl.get(), string.c_str(), static_cast(string.size())); @@ -190,10 +208,26 @@ static size_t CurlWriteCallback(char* data, size_t size, size_t nmemb, void* use return actual_size; } +static size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) +{ + auto* headers = static_cast(userdata); + std::string_view full_buffer = std::string_view{buffer, nitems}; + const size_t colon_pos = full_buffer.find(':'); + if (colon_pos == std::string::npos) + return nitems * size; + + const std::string_view key = full_buffer.substr(0, colon_pos); + const std::string_view value = StripWhitespace(full_buffer.substr(colon_pos + 1)); + + headers->emplace(std::string{key}, std::string{value}); + return nitems * size; +} + HttpRequest::Response HttpRequest::Impl::Fetch(const std::string& url, Method method, const Headers& headers, const u8* payload, size_t size, AllowedReturnCodes codes) { + m_response_headers.clear(); curl_easy_setopt(m_curl.get(), CURLOPT_POST, method == Method::POST); curl_easy_setopt(m_curl.get(), CURLOPT_URL, url.c_str()); if (method == Method::POST) @@ -215,6 +249,9 @@ HttpRequest::Response HttpRequest::Impl::Fetch(const std::string& url, Method me } curl_easy_setopt(m_curl.get(), CURLOPT_HTTPHEADER, list); + curl_easy_setopt(m_curl.get(), CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(m_curl.get(), CURLOPT_HEADERDATA, static_cast(&m_response_headers)); + std::vector buffer; curl_easy_setopt(m_curl.get(), CURLOPT_WRITEFUNCTION, CurlWriteCallback); curl_easy_setopt(m_curl.get(), CURLOPT_WRITEDATA, &buffer); diff --git a/Source/Core/Common/HttpRequest.h b/Source/Core/Common/HttpRequest.h index 0b8afb1b1b..e1c6c40539 100644 --- a/Source/Core/Common/HttpRequest.h +++ b/Source/Core/Common/HttpRequest.h @@ -40,6 +40,7 @@ public: void FollowRedirects(long max = 1); s32 GetLastResponseCode() const; std::string EscapeComponent(const std::string& string); + std::string GetHeaderValue(std::string_view name) const; Response Get(const std::string& url, const Headers& headers = {}, AllowedReturnCodes codes = AllowedReturnCodes::Ok_Only); Response Post(const std::string& url, const std::vector& payload, const Headers& headers = {}, diff --git a/Source/Core/Common/StringUtil.cpp b/Source/Core/Common/StringUtil.cpp index 033bc66407..c9100ff11a 100644 --- a/Source/Core/Common/StringUtil.cpp +++ b/Source/Core/Common/StringUtil.cpp @@ -693,4 +693,9 @@ bool CaseInsensitiveEquals(std::string_view a, std::string_view b) return std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) { return Common::ToLower(ca) == Common::ToLower(cb); }); } + +std::string BytesToHexString(std::span bytes) +{ + return fmt::format("{:02x}", fmt::join(bytes, "")); +} } // namespace Common diff --git a/Source/Core/Common/StringUtil.h b/Source/Core/Common/StringUtil.h index 1997fd0c5c..1699d9fa2f 100644 --- a/Source/Core/Common/StringUtil.h +++ b/Source/Core/Common/StringUtil.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -313,4 +314,5 @@ std::string GetEscapedHtml(std::string html); void ToLower(std::string* str); void ToUpper(std::string* str); bool CaseInsensitiveEquals(std::string_view a, std::string_view b); +std::string BytesToHexString(std::span bytes); } // namespace Common diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index f50185049a..c01e71bdf7 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -376,6 +376,9 @@ add_library(core IOS/Network/KD/VFF/VFFUtil.cpp IOS/Network/KD/VFF/VFFUtil.h IOS/Network/KD/WC24File.h + IOS/Network/KD/Mail/MailCommon.h + IOS/Network/KD/Mail/WC24Send.cpp + IOS/Network/KD/Mail/WC24Send.h IOS/Network/MACUtils.cpp IOS/Network/MACUtils.h IOS/Network/NCD/Manage.cpp diff --git a/Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h b/Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h new file mode 100644 index 0000000000..4bca377b89 --- /dev/null +++ b/Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h @@ -0,0 +1,74 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Common/CommonTypes.h" + +#include + +namespace IOS::HLE::NWC24::Mail +{ +constexpr u32 MAIL_LIST_MAGIC = 0x57635466; // WcTf + +#pragma pack(push, 1) +struct MailListHeader final +{ + u32 magic; // 'WcTf' 0x57635466 + u32 version; // 4 in Wii Menu 4.x + u32 number_of_mail; + u32 total_entries; + u32 total_size_of_messages; + u32 filesize; + u32 next_entry_id; + u32 next_entry_offset; + u32 unk1; + u32 vff_free_space; + std::array unk2; + std::array mail_flag; +}; +static_assert(sizeof(MailListHeader) == 128); + +struct MultipartEntry final +{ + u32 offset; + u32 size; +}; + +struct MailListEntry final +{ + u32 id; + u32 flag; + u32 msg_size; + u32 app_id; + u32 header_length; + u32 tag; + u32 wii_cmd; + // Never validated + u32 crc32; + u64 from_friend_code; + u32 minutes_since_1900; + u32 padding; + u8 always_1; + u8 number_of_multipart_entries; + u16 app_group; + u32 packed_from; + u32 packed_to; + u32 packed_subject; + u32 packed_charset; + u32 packed_transfer_encoding; + u32 message_offset; + // Set to message_length if content transfer encoding is not base64. + u32 encoded_length; + std::array multipart_entries; + std::array multipart_sizes; + std::array multipart_content_types; + u32 message_length; + u32 dwc_id; + u32 always_0x80000000; + u32 padding3; +}; +static_assert(sizeof(MailListEntry) == 128); + +#pragma pack(pop) +} // namespace IOS::HLE::NWC24::Mail diff --git a/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp new file mode 100644 index 0000000000..f889266ec6 --- /dev/null +++ b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp @@ -0,0 +1,68 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/Network/KD/Mail/WC24Send.h" +#include "Core/IOS/FS/FileSystem.h" +#include "Core/IOS/Uids.h" + +namespace IOS::HLE::NWC24::Mail +{ +constexpr const char SEND_LIST_PATH[] = "/" WII_WC24CONF_DIR "/mbox" + "/wc24send.ctl"; + +WC24SendList::WC24SendList(std::shared_ptr fs) : m_fs{std::move(fs)} +{ + ReadSendList(); +} + +void WC24SendList::ReadSendList() +{ + const auto file = m_fs->OpenFile(PID_KD, PID_KD, SEND_LIST_PATH, FS::Mode::Read); + if (!file || !file->Read(&m_data, 1)) + return; + + if (file->GetStatus()->size != SEND_LIST_SIZE) + { + ERROR_LOG_FMT(IOS_WC24, "The WC24 Send list file is not the correct size."); + return; + } + + const s32 file_error = CheckSendList(); + if (!file_error) + ERROR_LOG_FMT(IOS_WC24, "There is an error in the Send List for WC24 mail"); +} + +void WC24SendList::WriteSendList() const +{ + constexpr FS::Modes public_modes{FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite}; + m_fs->CreateFullPath(PID_KD, PID_KD, SEND_LIST_PATH, 0, public_modes); + const auto file = m_fs->CreateAndOpenFile(PID_KD, PID_KD, SEND_LIST_PATH, public_modes); + + if (!file || !file->Write(&m_data, 1)) + ERROR_LOG_FMT(IOS_WC24, "Failed to open or write WC24 Send list file"); +} + +bool WC24SendList::CheckSendList() const +{ + // 'WcTf' magic + if (Common::swap32(m_data.header.magic) != MAIL_LIST_MAGIC) + { + ERROR_LOG_FMT(IOS_WC24, "Send List magic mismatch ({} != {})", + Common::swap32(m_data.header.magic), MAIL_LIST_MAGIC); + return false; + } + + if (Common::swap32(m_data.header.version) != 4) + { + ERROR_LOG_FMT(IOS_WC24, "Send List version mismatch"); + return false; + } + + return true; +} + +std::string_view WC24SendList::GetMailFlag() const +{ + return {m_data.header.mail_flag.data(), m_data.header.mail_flag.size()}; +} +} // namespace IOS::HLE::NWC24::Mail diff --git a/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h new file mode 100644 index 0000000000..9f4e0399b1 --- /dev/null +++ b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h @@ -0,0 +1,52 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "Common/CommonPaths.h" +#include "Common/CommonTypes.h" +#include "Common/Logging/Log.h" +#include "Common/Swap.h" +#include "Core/IOS/Network/KD/Mail/MailCommon.h" +#include "Core/IOS/Network/KD/NWC24Config.h" + +namespace IOS::HLE +{ +namespace FS +{ +class FileSystem; +} +namespace NWC24::Mail +{ + +constexpr const char SEND_BOX_PATH[] = "/" WII_WC24CONF_DIR "/mbox" + "/wc24send.mbx"; +class WC24SendList final +{ +public: + explicit WC24SendList(std::shared_ptr fs); + + void ReadSendList(); + bool CheckSendList() const; + void WriteSendList() const; + + std::string_view GetMailFlag() const; + +private: + static constexpr u32 MAX_ENTRIES = 127; + static constexpr u32 SEND_LIST_SIZE = 16384; + +#pragma pack(push, 1) + struct SendList final + { + MailListHeader header; + std::array entries; + }; + static_assert(sizeof(SendList) == SEND_LIST_SIZE); +#pragma pack(pop) + + SendList m_data; + std::shared_ptr m_fs; +}; +} // namespace NWC24::Mail +} // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp b/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp index 56b1d747cd..940a5360dd 100644 --- a/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp +++ b/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp @@ -216,4 +216,16 @@ void NWC24Config::SetEmail(const char* email) strncpy(m_data.email, email, MAX_EMAIL_LENGTH); m_data.email[MAX_EMAIL_LENGTH - 1] = '\0'; } + +std::string_view NWC24Config::GetMlchkid() const +{ + const size_t size = strnlen(m_data.mlchkid, MAX_MLCHKID_LENGTH); + return {m_data.mlchkid, size}; +} + +std::string NWC24Config::GetCheckURL() const +{ + const size_t size = strnlen(m_data.http_urls[1], MAX_URL_LENGTH); + return {m_data.http_urls[1], size}; +} } // namespace IOS::HLE::NWC24 diff --git a/Source/Core/Core/IOS/Network/KD/NWC24Config.h b/Source/Core/Core/IOS/Network/KD/NWC24Config.h index 0f3682449e..2b05d6dfd7 100644 --- a/Source/Core/Core/IOS/Network/KD/NWC24Config.h +++ b/Source/Core/Core/IOS/Network/KD/NWC24Config.h @@ -69,6 +69,9 @@ public: u32 Checksum() const; void SetChecksum(u32 checksum); + std::string_view GetMlchkid() const; + std::string GetCheckURL() const; + NWC24CreationStage CreationStage() const; void SetCreationStage(NWC24CreationStage creation_stage); @@ -92,6 +95,7 @@ private: MAX_URL_LENGTH = 0x80, MAX_EMAIL_LENGTH = 0x40, MAX_PASSWORD_LENGTH = 0x20, + MAX_MLCHKID_LENGTH = 0x24, }; #pragma pack(push, 1) @@ -104,7 +108,7 @@ private: NWC24CreationStage creation_stage; char email[MAX_EMAIL_LENGTH]; char paswd[MAX_PASSWORD_LENGTH]; - char mlchkid[0x24]; + char mlchkid[MAX_MLCHKID_LENGTH]; char http_urls[URL_COUNT][MAX_URL_LENGTH]; u8 reserved[0xDC]; u32 enable_booting; diff --git a/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp b/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp index b59c365155..d28f1a3946 100644 --- a/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp +++ b/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp @@ -12,11 +12,14 @@ #include "Common/BitUtils.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" +#include "Common/Crypto/HMAC.h" #include "Common/FileUtil.h" #include "Common/Logging/Log.h" #include "Common/NandPaths.h" #include "Common/SettingsHandler.h" +#include "Common/Random.h" +#include "Common/ScopeGuard.h" #include "Core/CommonTitles.h" #include "Core/HW/Memmap.h" #include "Core/IOS/FS/FileSystem.h" @@ -149,7 +152,8 @@ s32 NWC24MakeUserID(u64* nwc24_id, u32 hollywood_id, u16 id_ctr, HardwareModel h } // Anonymous namespace NetKDRequestDevice::NetKDRequestDevice(EmulationKernel& ios, const std::string& device_name) - : EmulationDevice(ios, device_name), config{ios.GetFS()}, m_dl_list{ios.GetFS()} + : EmulationDevice(ios, device_name), m_config{ios.GetFS()}, m_dl_list{ios.GetFS()}, + m_send_list{ios.GetFS()} { // Enable all NWC24 permissions m_scheduler_buffer[1] = Common::swap32(-1); @@ -161,6 +165,12 @@ NetKDRequestDevice::NetKDRequestDevice(EmulationKernel& ios, const std::string& m_async_replies.emplace(AsyncReply{task.request, reply.return_value}); } }); + + m_handle_mail = !ios.GetIOSC().IsUsingDefaultId(); + m_scheduler_work_queue.Reset("WiiConnect24 Scheduler Worker", + [](std::function task) { task(); }); + + m_scheduler_timer_thread = std::thread([this] { SchedulerTimer(); }); } NetKDRequestDevice::~NetKDRequestDevice() @@ -168,6 +178,16 @@ NetKDRequestDevice::~NetKDRequestDevice() auto socket_manager = GetEmulationKernel().GetSocketManager(); if (socket_manager) socket_manager->Clean(); + + { + std::lock_guard lg(m_scheduler_lock); + if (!m_scheduler_timer_thread.joinable()) + return; + + m_shutdown_event.Set(); + } + + m_scheduler_timer_thread.join(); } void NetKDRequestDevice::Update() @@ -183,6 +203,78 @@ void NetKDRequestDevice::Update() } } +void NetKDRequestDevice::SchedulerTimer() +{ + u32 mail_time_state = 0; + u32 download_time_state = 0; + Common::SetCurrentThreadName("KD Scheduler Timer"); + while (true) + { + { + std::lock_guard lg(m_scheduler_lock); + if (m_mail_span <= mail_time_state && m_handle_mail) + { + m_scheduler_work_queue.EmplaceItem([this] { SchedulerWorker(SchedulerEvent::Mail); }); + INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: Dispatching Mail Task from Scheduler"); + mail_time_state = 0; + } + + if (m_download_span <= download_time_state) + { + INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: Dispatching Download Task from Scheduler"); + m_scheduler_work_queue.EmplaceItem([this] { SchedulerWorker(SchedulerEvent::Download); }); + download_time_state = 0; + } + } + + if (m_shutdown_event.WaitFor(std::chrono::minutes{1})) + return; + + mail_time_state++; + download_time_state++; + } +} + +void NetKDRequestDevice::SchedulerWorker(const SchedulerEvent event) +{ + if (event == SchedulerEvent::Download) + { + // TODO: Implement downloader part of scheduler + return; + } + else + { + if (!m_config.IsRegistered()) + return; + + u32 mail_flag{}; + u32 interval{}; + + NWC24::ErrorCode code = KDCheckMail(&mail_flag, &interval); + if (code != NWC24::WC24_OK) + { + LogError(ErrorType::CheckMail, code); + } + } +} + +std::string NetKDRequestDevice::GetValueFromCGIResponse(const std::string& response, + const std::string& key) +{ + const std::vector raw_fields = SplitString(response, '\n'); + for (const std::string& field : raw_fields) + { + const std::vector key_value = SplitString(field, '='); + if (key_value.size() != 2) + continue; + + if (key_value[0] == key) + return std::string{StripWhitespace(key_value[1])}; + } + + return {}; +} + void NetKDRequestDevice::LogError(ErrorType error_type, s32 error_code) { s32 new_code{}; @@ -200,6 +292,9 @@ void NetKDRequestDevice::LogError(ErrorType error_type, s32 error_code) case ErrorType::Server: new_code = -(117000 + error_code); break; + case ErrorType::CheckMail: + new_code = -(102200 - error_code); + break; } std::lock_guard lg(m_scheduler_buffer_lock); @@ -211,6 +306,100 @@ void NetKDRequestDevice::LogError(ErrorType error_type, s32 error_code) m_scheduler_buffer[2] = Common::swap32(new_code); } +NWC24::ErrorCode NetKDRequestDevice::KDCheckMail(u32* mail_flag, u32* interval) +{ + bool success = false; + Common::ScopeGuard state_guard([&] { + std::lock_guard lg(m_scheduler_buffer_lock); + if (success) + { + // m_scheduler_buffer[11] contains the amount of times we have checked for mail this IOS + // session. + m_scheduler_buffer[11] = Common::swap32(Common::swap32(m_scheduler_buffer[11]) + 1); + } + m_scheduler_buffer[4] = static_cast(CurrentFunction::None); + }); + + { + std::lock_guard lg(m_scheduler_buffer_lock); + m_scheduler_buffer[4] = Common::swap32(static_cast(CurrentFunction::Check)); + } + + u64 random_number{}; + Common::Random::Generate(&random_number, sizeof(u64)); + const std::string form_data( + fmt::format("mlchkid={}&chlng={}", m_config.GetMlchkid(), random_number)); + const Common::HttpRequest::Response response = m_http.Post(m_config.GetCheckURL(), form_data); + + if (!response) + { + ERROR_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_CHECK_MAIL_NOW: Failed to request data at {}.", + m_config.GetCheckURL()); + return NWC24::WC24_ERR_SERVER; + } + + const std::string response_str = {response->begin(), response->end()}; + const std::string code = GetValueFromCGIResponse(response_str, "cd"); + if (code != "100") + { + ERROR_LOG_FMT( + IOS_WC24, + "NET_KD_REQ: IOCTL_NWC24_CHECK_MAIL_NOW: Mail server returned non-success code: {}", code); + return NWC24::WC24_ERR_SERVER; + } + + const std::string server_hmac = GetValueFromCGIResponse(response_str, "res"); + const std::string str_mail_flag = GetValueFromCGIResponse(response_str, "mail.flag"); + const std::string str_interval = GetValueFromCGIResponse(response_str, "interval"); + DEBUG_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_CHECK_MAIL_NOW: Server HMAC: {}", server_hmac); + + // On a real Wii, a response to a challenge is expected and would be verified by KD. + const std::string hmac_message = + fmt::format("{}\nw{}\n{}\n{}", random_number, m_config.Id(), str_mail_flag, str_interval); + std::array hashed{}; + Common::HMAC::HMACWithSHA1( + MAIL_CHECK_KEY, + std::span(reinterpret_cast(hmac_message.data()), hmac_message.size()), + hashed.data()); + + // On a real Wii, strncmp is used to compare both hashes. This means that it is a case-sensitive + // comparison. KD will generate a lowercase hash as well as expect a lowercase hash from the + // server. + if (Common::BytesToHexString(hashed) != server_hmac) + { + ERROR_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_CHECK_MAIL_NOW: Server HMAC is invalid."); + return NWC24::WC24_ERR_SERVER; + } + + *mail_flag = std::strncmp(str_mail_flag.data(), m_send_list.GetMailFlag().data(), 22) != 0; + + { + std::lock_guard scheduler_lg(m_scheduler_lock); + bool did_parse = TryParse(m_http.GetHeaderValue("X-Wii-Mail-Check-Span"), interval); + if (did_parse) + { + if (*interval == 0) + { + *interval = 1; + } + + m_mail_span = *interval; + } + + did_parse = TryParse(m_http.GetHeaderValue("X-Wii-Download-Span"), &m_download_span); + if (did_parse) + { + if (m_download_span == 0) + { + m_download_span = 1; + } + } + } + + success = true; + return NWC24::WC24_OK; +} + NWC24::ErrorCode NetKDRequestDevice::KDDownload(const u16 entry_index, const std::optional subtask_id) { @@ -292,6 +481,21 @@ NWC24::ErrorCode NetKDRequestDevice::KDDownload(const u16 entry_index, return reply; } +IPCReply NetKDRequestDevice::HandleNWC24CheckMailNow(const IOCtlRequest& request) +{ + auto& system = GetSystem(); + auto& memory = system.GetMemory(); + + u32 mail_flag{}; + u32 interval{}; + const NWC24::ErrorCode reply = KDCheckMail(&mail_flag, &interval); + + WriteReturnValue(reply, request.buffer_out); + memory.Write_U32(mail_flag, request.buffer_out + 4); + memory.Write_U32(interval, request.buffer_out + 8); + return IPCReply(IPC_SUCCESS); +} + IPCReply NetKDRequestDevice::HandleNWC24DownloadNowEx(const IOCtlRequest& request) { m_dl_list.ReadDlList(); @@ -430,7 +634,7 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) case IOCTL_NWC24_REQUEST_GENERATED_USER_ID: // (Input: none, Output: 32 bytes) INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_REQUEST_GENERATED_USER_ID"); - if (config.IsCreated()) + if (m_config.IsCreated()) { const std::string settings_file_path = Common::GetTitleDataPath(Titles::SYSTEM_MENU) + "/" WII_SETTING; @@ -451,19 +655,19 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) if (!area.empty() && !model.empty()) { const u8 area_code = GetAreaCode(area); - const u8 id_ctr = u8(config.IdGen()); + const u8 id_ctr = u8(m_config.IdGen()); const HardwareModel hardware_model = GetHardwareModel(model); const u32 hollywood_id = m_ios.GetIOSC().GetDeviceId(); u64 user_id = 0; const s32 ret = NWC24MakeUserID(&user_id, hollywood_id, id_ctr, hardware_model, area_code); - config.SetId(user_id); - config.IncrementIdGen(); - config.SetCreationStage(NWC24::NWC24CreationStage::Generated); - config.SetChecksum(config.CalculateNwc24ConfigChecksum()); - config.WriteConfig(); - config.WriteCBK(); + m_config.SetId(user_id); + m_config.IncrementIdGen(); + m_config.SetCreationStage(NWC24::NWC24CreationStage::Generated); + m_config.SetChecksum(m_config.CalculateNwc24ConfigChecksum()); + m_config.WriteConfig(); + m_config.WriteCBK(); WriteReturnValue(ret, request.buffer_out); } @@ -473,16 +677,16 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) WriteReturnValue(NWC24::WC24_ERR_FATAL, request.buffer_out); } } - else if (config.IsGenerated()) + else if (m_config.IsGenerated()) { WriteReturnValue(NWC24::WC24_ERR_ID_GENERATED, request.buffer_out); } - else if (config.IsRegistered()) + else if (m_config.IsRegistered()) { WriteReturnValue(NWC24::WC24_ERR_ID_REGISTERED, request.buffer_out); } - memory.Write_U64(config.Id(), request.buffer_out + 4); - memory.Write_U32(u32(config.CreationStage()), request.buffer_out + 0xC); + memory.Write_U64(m_config.Id(), request.buffer_out + 4); + memory.Write_U32(u32(m_config.CreationStage()), request.buffer_out + 0xC); break; case IOCTL_NWC24_GET_SCHEDULER_STAT: @@ -498,8 +702,8 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) request.buffer_out_size); // On a real Wii, GetSchedulerStat copies memory containing a list of error codes recorded by - // KD among other things. In most instances there will never be more than one error code - // recorded as we do not have a scheduler. + // KD among other things. + std::lock_guard lg(m_scheduler_buffer_lock); const u32 out_size = std::min(request.buffer_out_size, 256U); memory.CopyToEmu(request.buffer_out, m_scheduler_buffer.data(), out_size); break; @@ -509,6 +713,9 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_SAVE_MAIL_NOW - NI"); break; + case IOCTL_NWC24_CHECK_MAIL_NOW: + return LaunchAsyncTask(&NetKDRequestDevice::HandleNWC24CheckMailNow, request); + case IOCTL_NWC24_DOWNLOAD_NOW_EX: return LaunchAsyncTask(&NetKDRequestDevice::HandleNWC24DownloadNowEx, request); diff --git a/Source/Core/Core/IOS/Network/KD/NetKDRequest.h b/Source/Core/Core/IOS/Network/KD/NetKDRequest.h index 035b4a286b..4f7a780359 100644 --- a/Source/Core/Core/IOS/Network/KD/NetKDRequest.h +++ b/Source/Core/Core/IOS/Network/KD/NetKDRequest.h @@ -9,9 +9,11 @@ #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" +#include "Common/Event.h" #include "Common/HttpRequest.h" #include "Common/WorkQueueThread.h" #include "Core/IOS/Device.h" +#include "Core/IOS/Network/KD/Mail/WC24Send.h" #include "Core/IOS/Network/KD/NWC24Config.h" #include "Core/IOS/Network/KD/NWC24DL.h" @@ -26,6 +28,7 @@ public: NetKDRequestDevice(EmulationKernel& ios, const std::string& device_name); IPCReply HandleNWC24DownloadNowEx(const IOCtlRequest& request); NWC24::ErrorCode KDDownload(const u16 entry_index, const std::optional subtask_id); + IPCReply HandleNWC24CheckMailNow(const IOCtlRequest& request); ~NetKDRequestDevice() override; std::optional IOCtl(const IOCtlRequest& request) override; @@ -51,19 +54,48 @@ private: return std::nullopt; } + enum class CurrentFunction : u32 + { + None = 0, + Account = 1, + Check = 2, + Receive = 3, + Send = 5, + Save = 6, + Download = 7, + }; + enum class ErrorType { Account, KD_Download, Client, Server, + CheckMail, }; - void LogError(ErrorType error_type, s32 error_code); + enum class SchedulerEvent + { + Mail, + Download, + }; - NWC24::NWC24Config config; + NWC24::ErrorCode KDCheckMail(u32* mail_flag, u32* interval); + + void LogError(ErrorType error_type, s32 error_code); + void SchedulerTimer(); + void SchedulerWorker(SchedulerEvent event); + + static std::string GetValueFromCGIResponse(const std::string& response, const std::string& key); + static constexpr std::array MAIL_CHECK_KEY = {0xce, 0x4c, 0xf2, 0x9a, 0x3d, 0x6b, 0xe1, + 0xc2, 0x61, 0x91, 0x72, 0xb5, 0xcb, 0x29, + 0x8c, 0x89, 0x72, 0xd4, 0x50, 0xad}; + + NWC24::NWC24Config m_config; NWC24::NWC24Dl m_dl_list; + NWC24::Mail::WC24SendList m_send_list; Common::WorkQueueThread m_work_queue; + Common::WorkQueueThread> m_scheduler_work_queue; std::mutex m_async_reply_lock; std::mutex m_scheduler_buffer_lock; std::queue m_async_replies; @@ -71,5 +103,11 @@ private: std::array m_scheduler_buffer{}; // TODO: Maybe move away from Common::HttpRequest? Common::HttpRequest m_http{std::chrono::minutes{1}}; + u32 m_download_span = 2; + u32 m_mail_span = 1; + bool m_handle_mail; + Common::Event m_shutdown_event; + std::mutex m_scheduler_lock; + std::thread m_scheduler_timer_thread; }; } // namespace IOS::HLE diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 1b44f15ec4..be2c076148 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -35,6 +35,7 @@ + @@ -360,6 +361,8 @@ + + @@ -757,6 +760,7 @@ + @@ -999,6 +1003,7 @@ +