diff --git a/Source/Core/Common/HttpRequest.cpp b/Source/Core/Common/HttpRequest.cpp index afc6271542..cf26710372 100644 --- a/Source/Core/Common/HttpRequest.cpp +++ b/Source/Core/Common/HttpRequest.cpp @@ -33,7 +33,8 @@ public: void FollowRedirects(long max); s32 GetLastResponseCode(); Response Fetch(const std::string& url, Method method, const Headers& headers, const u8* payload, - size_t size, AllowedReturnCodes codes = AllowedReturnCodes::Ok_Only); + size_t size, AllowedReturnCodes codes = AllowedReturnCodes::Ok_Only, + std::span multiform = {}); static int CurlProgressCallback(Impl* impl, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); @@ -174,6 +175,13 @@ void HttpRequest::Impl::UseIPv4() curl_easy_setopt(m_curl.get(), CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); } +HttpRequest::Response HttpRequest::PostMultiform(const std::string& url, + std::span multiform, + const Headers& headers, AllowedReturnCodes codes) +{ + return m_impl->Fetch(url, Impl::Method::POST, headers, nullptr, 0, codes, multiform); +} + void HttpRequest::Impl::FollowRedirects(long max) { curl_easy_setopt(m_curl.get(), CURLOPT_FOLLOWLOCATION, 1); @@ -225,17 +233,33 @@ static size_t header_callback(char* buffer, size_t size, size_t nitems, void* us HttpRequest::Response HttpRequest::Impl::Fetch(const std::string& url, Method method, const Headers& headers, const u8* payload, - size_t size, AllowedReturnCodes codes) + size_t size, AllowedReturnCodes codes, + std::span multiform) { 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) + if (method == Method::POST && multiform.empty()) { curl_easy_setopt(m_curl.get(), CURLOPT_POSTFIELDS, payload); curl_easy_setopt(m_curl.get(), CURLOPT_POSTFIELDSIZE, size); } + curl_mime* form = nullptr; + Common::ScopeGuard multiform_guard{[&form] { curl_mime_free(form); }}; + if (!multiform.empty()) + { + form = curl_mime_init(m_curl.get()); + for (const auto& value : multiform) + { + curl_mimepart* part = curl_mime_addpart(form); + curl_mime_name(part, value.name.c_str()); + curl_mime_data(part, value.data.c_str(), value.data.size()); + } + + curl_easy_setopt(m_curl.get(), CURLOPT_MIMEPOST, form); + } + curl_slist* list = nullptr; Common::ScopeGuard list_guard{[&list] { curl_slist_free_all(list); }}; for (const auto& [name, value] : headers) diff --git a/Source/Core/Common/HttpRequest.h b/Source/Core/Common/HttpRequest.h index e1c6c40539..4f1a9798d5 100644 --- a/Source/Core/Common/HttpRequest.h +++ b/Source/Core/Common/HttpRequest.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -35,6 +36,12 @@ public: using Response = std::optional>; using Headers = std::map>; + struct Multiform + { + std::string name; + std::string data; + }; + void SetCookies(const std::string& cookies); void UseIPv4(); void FollowRedirects(long max = 1); @@ -48,6 +55,10 @@ public: Response Post(const std::string& url, const std::string& payload, const Headers& headers = {}, AllowedReturnCodes codes = AllowedReturnCodes::Ok_Only); + Response PostMultiform(const std::string& url, std::span multiform, + const Headers& headers = {}, + AllowedReturnCodes codes = AllowedReturnCodes::Ok_Only); + private: class Impl; std::unique_ptr m_impl; diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 60650a573c..1dcbc14ce7 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -383,6 +383,8 @@ add_library(core IOS/Network/KD/VFF/VFFUtil.h IOS/Network/KD/WC24File.h IOS/Network/KD/Mail/MailCommon.h + IOS/Network/KD/Mail/WC24FriendList.cpp + IOS/Network/KD/Mail/WC24FriendList.h IOS/Network/KD/Mail/WC24Send.cpp IOS/Network/KD/Mail/WC24Send.h IOS/Network/MACUtils.cpp diff --git a/Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h b/Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h index 4bca377b89..6919c9cfec 100644 --- a/Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h +++ b/Source/Core/Core/IOS/Network/KD/Mail/MailCommon.h @@ -11,6 +11,11 @@ namespace IOS::HLE::NWC24::Mail { constexpr u32 MAIL_LIST_MAGIC = 0x57635466; // WcTf +inline u32 CalculateFileOffset(u32 index) +{ + return Common::swap32(128 + (index * 128)); +} + #pragma pack(push, 1) struct MailListHeader final { diff --git a/Source/Core/Core/IOS/Network/KD/Mail/WC24FriendList.cpp b/Source/Core/Core/IOS/Network/KD/Mail/WC24FriendList.cpp new file mode 100644 index 0000000000..17b08bf64a --- /dev/null +++ b/Source/Core/Core/IOS/Network/KD/Mail/WC24FriendList.cpp @@ -0,0 +1,93 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/IOS/Network/KD/Mail/WC24FriendList.h" +#include "Core/IOS/FS/FileSystem.h" +#include "Core/IOS/Uids.h" + +namespace IOS::HLE::NWC24::Mail +{ +WC24FriendList::WC24FriendList(std::shared_ptr fs) : m_fs{std::move(fs)} +{ + ReadFriendList(); +} + +void WC24FriendList::ReadFriendList() +{ + const auto file = m_fs->OpenFile(PID_KD, PID_KD, FRIEND_LIST_PATH, FS::Mode::Read); + if (!file || !file->Read(&m_data, 1)) + return; + + const bool success = CheckFriendList(); + if (!success) + ERROR_LOG_FMT(IOS_WC24, "There is an error in the Receive List for WC24 mail"); +} + +void WC24FriendList::WriteFriendList() const +{ + constexpr FS::Modes public_modes{FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite}; + m_fs->CreateFullPath(PID_KD, PID_KD, FRIEND_LIST_PATH, 0, public_modes); + const auto file = m_fs->CreateAndOpenFile(PID_KD, PID_KD, FRIEND_LIST_PATH, public_modes); + + if (!file || !file->Write(&m_data, 1)) + ERROR_LOG_FMT(IOS_WC24, "Failed to open or write WC24 Receive list file"); +} + +bool WC24FriendList::CheckFriendList() const +{ + // 'WcFl' magic + if (Common::swap32(m_data.header.magic) != FRIEND_LIST_MAGIC) + { + ERROR_LOG_FMT(IOS_WC24, "Receive List magic mismatch ({} != {})", + Common::swap32(m_data.header.magic), FRIEND_LIST_MAGIC); + return false; + } + + return true; +} + +bool WC24FriendList::DoesFriendExist(u64 friend_id) const +{ + return std::any_of(m_data.friend_codes.cbegin(), m_data.friend_codes.cend(), + [&friend_id](const u64 v) { return v == friend_id; }); +} + +std::vector WC24FriendList::GetUnconfirmedFriends() const +{ + std::vector friends{}; + for (u32 i = 0; i < MAX_ENTRIES; i++) + { + if (static_cast(Common::swap32(m_data.entries[i].status)) == + FriendStatus::Unconfirmed && + static_cast(Common::swap32(m_data.entries[i].friend_type)) == FriendType::Wii) + { + friends.push_back(Common::swap64(m_data.friend_codes.at(i))); + } + } + + return friends; +} + +u64 WC24FriendList::ConvertEmailToFriendCode(std::string_view email) +{ + u32 upper = 0x80; + u32 lower{}; + + u32 idx{}; + for (char chr : email) + { + if (idx == 7) + { + upper = upper | (email.size() & 0x1f); + break; + } + + lower = (upper | chr) >> 0x18 | (lower | lower >> 0x1f) << 8; + upper = (upper | chr) * 0x100; + idx++; + } + + return u64{lower} << 32 | upper; +} + +} // namespace IOS::HLE::NWC24::Mail diff --git a/Source/Core/Core/IOS/Network/KD/Mail/WC24FriendList.h b/Source/Core/Core/IOS/Network/KD/Mail/WC24FriendList.h new file mode 100644 index 0000000000..5c8ec241cf --- /dev/null +++ b/Source/Core/Core/IOS/Network/KD/Mail/WC24FriendList.h @@ -0,0 +1,98 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/CommonPaths.h" +#include "Common/CommonTypes.h" +#include "Common/Logging/Log.h" +#include "Common/Swap.h" +#include "Core/IOS/Network/KD/NWC24Config.h" + +namespace IOS::HLE +{ +namespace FS +{ +class FileSystem; +} +namespace NWC24::Mail +{ +constexpr const char FRIEND_LIST_PATH[] = "/" WII_WC24CONF_DIR "/nwc24fl.bin"; +class WC24FriendList final +{ +public: + explicit WC24FriendList(std::shared_ptr fs); + static u64 ConvertEmailToFriendCode(std::string_view email); + + void ReadFriendList(); + bool CheckFriendList() const; + void WriteFriendList() const; + + bool DoesFriendExist(u64 friend_id) const; + std::vector GetUnconfirmedFriends() const; + +private: + static constexpr u32 FRIEND_LIST_MAGIC = 0x5763466C; // WcFl + static constexpr u32 MAX_ENTRIES = 100; + +#pragma pack(push, 1) + struct FriendListHeader final + { + u32 magic; // 'WcFl' 0x5763466C + u32 version; + u32 max_friend_entries; + u32 number_of_friends; + char padding[48]; + }; + static_assert(sizeof(FriendListHeader) == 64); + static_assert(std::is_trivially_copyable_v); + + enum class FriendType : u32 + { + None, + Wii, + Email + }; + + enum class FriendStatus : u32 + { + None, + Unconfirmed, + Confirmed, + Declined + }; + + struct FriendListEntry final + { + u32 friend_type; + u32 status; + char nickname[24]; + u32 mii_id; + u32 system_id; + char reserved[24]; + char email_or_code[96]; + char padding[160]; + }; + static_assert(sizeof(FriendListEntry) == 320); + static_assert(std::is_trivially_copyable_v); + + struct FriendList final + { + FriendListHeader header; + std::array friend_codes; + std::array entries; + }; + static_assert(sizeof(FriendList) == 32864); + static_assert(std::is_trivially_copyable_v); +#pragma pack(pop) + + FriendList m_data; + std::shared_ptr m_fs; +}; +} // namespace NWC24::Mail +} // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp index faf4e6a14f..76b3fa639d 100644 --- a/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp +++ b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.cpp @@ -3,8 +3,10 @@ #include "Core/IOS/Network/KD/Mail/WC24Send.h" #include "Core/IOS/FS/FileSystem.h" +#include "Core/IOS/Network/KD/VFF/VFFUtil.h" #include "Core/IOS/Uids.h" +#include #include "Common/Assert.h" namespace IOS::HLE::NWC24::Mail @@ -44,7 +46,28 @@ bool WC24SendList::ReadSendList() return false; } - return CheckSendList(); + // Make sure that next_entry_offset is not out of bounds. + if (m_data.header.next_entry_offset % 128 != 0 || + m_data.header.next_entry_offset > + sizeof(MailListEntry) * (MAX_ENTRIES - 1) + sizeof(MailListEntry)) + { + const std::optional next_entry_index = GetNextFreeEntryIndex(); + if (!next_entry_index) + { + // If there are no free entries, we will have to overwrite an entry. + m_data.header.next_entry_offset = Common::swap32(128); + } + else + { + m_data.header.next_entry_offset = CalculateFileOffset(next_entry_index.value()); + } + } + + const s32 file_error = CheckSendList(); + if (!file_error) + ERROR_LOG_FMT(IOS_WC24, "There is an error in the Send List for WC24 mail"); + + return true; } bool WC24SendList::IsDisabled() const @@ -82,6 +105,147 @@ bool WC24SendList::CheckSendList() const return true; } +u32 WC24SendList::GetNumberOfMail() const +{ + ASSERT(!IsDisabled()); + return Common::swap32(m_data.header.number_of_mail); +} + +u32 WC24SendList::GetEntryId(u32 entry_index) const +{ + ASSERT(!IsDisabled()); + return Common::swap32(m_data.entries[entry_index].id); +} + +u32 WC24SendList::GetMailSize(u32 index) const +{ + ASSERT(!IsDisabled()); + return Common::swap32(m_data.entries[index].msg_size); +} + +ErrorCode WC24SendList::DeleteMessage(u32 index) +{ + ASSERT(!IsDisabled()); + ErrorCode error = NWC24::DeleteFileFromVFF(NWC24::Mail::SEND_BOX_PATH, GetMailPath(index), m_fs); + if (error != WC24_OK) + return error; + + // Fix up the header then clear the entry. + m_data.header.number_of_mail = Common::swap32(Common::swap32(m_data.header.number_of_mail) - 1); + m_data.header.next_entry_id = Common::swap32(GetEntryId(index)); + m_data.header.next_entry_offset = CalculateFileOffset(index); + m_data.header.total_size_of_messages = + Common::swap32(m_data.header.total_size_of_messages) - GetMailSize(index); + + std::memset(&m_data.entries[index], 0, sizeof(MailListEntry)); + return WC24_OK; +} + +std::string WC24SendList::GetMailPath(u32 index) const +{ + return fmt::format("mb/s{:07d}.msg", GetEntryId(index)); +} + +u32 WC24SendList::GetNextEntryId() const +{ + ASSERT(!IsDisabled()); + return Common::swap32(m_data.header.next_entry_id); +} + +u32 WC24SendList::GetNextEntryIndex() const +{ + ASSERT(!IsDisabled()); + return (Common::swap32(m_data.header.next_entry_offset) - 128) / 128; +} + +std::vector WC24SendList::GetMailToSend() const +{ + ASSERT(!IsDisabled()); + // The list is not guaranteed to have all entries consecutively. + // As such we must find the populated entries for the specified number of mails. + const u32 mail_count = std::min(GetNumberOfMail(), 16U); + u32 found{}; + + std::vector mails{}; + for (u32 index = 0; index < MAX_ENTRIES; index++) + { + if (found == mail_count) + break; + + if (GetEntryId(index) != 0) + { + mails.emplace_back(index); + found++; + } + } + + return mails; +} + +std::optional WC24SendList::GetNextFreeEntryIndex() const +{ + for (u32 index = 0; index < MAX_ENTRIES; index++) + { + if (GetEntryId(index) == 0) + return index; + } + + return std::nullopt; +} + +ErrorCode WC24SendList::AddRegistrationMessages(const WC24FriendList& friend_list, u64 sender) +{ + ASSERT(!IsDisabled()); + // It is possible that the user composed a message before SendMail was called. + ReadSendList(); + + const std::vector unconfirmed_friends = friend_list.GetUnconfirmedFriends(); + for (const u64 code : unconfirmed_friends) + { + const u32 entry_index = GetNextEntryIndex(); + const u32 msg_id = GetNextEntryId(); + m_data.entries[entry_index].id = Common::swap32(msg_id); + + std::time_t t = std::time(nullptr); + + const std::string formatted_message = + fmt::format(MAIL_REGISTRATION_STRING, sender, code, fmt::gmtime(t)); + std::vector message{formatted_message.begin(), formatted_message.end()}; + NWC24::ErrorCode reply = + NWC24::WriteToVFF(NWC24::Mail::SEND_BOX_PATH, GetMailPath(entry_index), m_fs, message); + + if (reply != WC24_OK) + { + ERROR_LOG_FMT(IOS_WC24, "Error writing registration message to VFF"); + return reply; + } + + NOTICE_LOG_FMT(IOS_WC24, "Issued registration message for Wii Friend: {}", code); + + // Update the header and some fields in the body + m_data.entries[entry_index].msg_size = Common::swap32(static_cast(message.size())); + m_data.header.number_of_mail = Common::swap32(GetNumberOfMail() + 1); + m_data.header.next_entry_id = Common::swap32(msg_id + 1); + m_data.header.total_size_of_messages = + Common::swap32(m_data.header.total_size_of_messages) + static_cast(message.size()); + + const std::optional next_entry_index = GetNextFreeEntryIndex(); + if (!next_entry_index) + { + // If there are no free entries, we overwrite the first entry. + m_data.header.next_entry_offset = Common::swap32(128); + } + else + { + m_data.header.next_entry_offset = CalculateFileOffset(next_entry_index.value()); + } + } + + // Only flush on success. + WriteSendList(); + return WC24_OK; +} + std::string_view WC24SendList::GetMailFlag() const { ASSERT(!IsDisabled()); diff --git a/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h index 391511cafd..056372d147 100644 --- a/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h +++ b/Source/Core/Core/IOS/Network/KD/Mail/WC24Send.h @@ -3,13 +3,16 @@ #pragma once +#include #include +#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/Mail/WC24FriendList.h" #include "Core/IOS/Network/KD/NWC24Config.h" namespace IOS::HLE @@ -34,11 +37,56 @@ public: bool IsDisabled() const; std::string_view GetMailFlag() const; + u32 GetNumberOfMail() const; + std::vector GetMailToSend() const; + u32 GetEntryId(u32 entry_index) const; + u32 GetMailSize(u32 index) const; + ErrorCode DeleteMessage(u32 index); + std::string GetMailPath(u32 index) const; + u32 GetNextEntryId() const; + u32 GetNextEntryIndex() const; + std::optional GetNextFreeEntryIndex() const; + + ErrorCode AddRegistrationMessages(const WC24FriendList& friend_list, u64 sender); private: static constexpr u32 MAX_ENTRIES = 127; static constexpr u32 SEND_LIST_SIZE = 16384; + // Format for the message Wii Mail sends when trying to register a Wii Friend. + // Most fields can be static such as the X-Wii-AppId which is the Wii Menu, + // X-Wii-Cmd which is the registration command, and the attached file which is + // just 128 bytes of base64 encoded 0 bytes. That file is supposed to be friend profile data which + // is written to nwc24fl.bin, although it has been observed to always be 0. + static constexpr char MAIL_REGISTRATION_STRING[] = + "MAIL FROM: {0:016d}@wii.com\r\n" + "RCPT TO: {1:016d}wii.com\r\n" + "DATA\r\n" + "Date: {2:%a, %d %b %Y %X} GMT\r\n" + "From: {0:016d}@wii.com\r\n" + "To: {1:016d}@wii.com\r\n" + "Message-Id: <00002000B0DF6BB47FE0303E0DB0D@wii.com>\r\n" + "Subject: WC24 Cmd Message\r\n" + "X-Wii-AppId: 0-00000001-0001\r\n" + "X-Wii-Cmd: 80010001\r\n" + "MIME-Version: 1.0\r\n" + "Content-Type: multipart/mixed;\r\n " + "boundary=\"Boundary-NWC24-041B6CE500012\"\r\n" + "--Boundary-NWC24-041B6CE500012\r\n" + "Content-Type: text/plain; charset=us-ascii\r\n" + "Content-Transfer-Encoding: 7bit\r\n" + "WC24 Cmd Message\r\n" + "--Boundary-NWC24-041B6CE500012\r\n" + "Content-Type: application/octet-stream;\r\n " + "name=a0000018.dat\r\n" + "Content-Transfer-Encoding: base64\r\n" + "Content-Disposition: attachment;\r\n " + "filename=a0000018.dat\r\n\r\n " + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + "\r\n\r\n" + "--Boundary-NWC24-041B6CE500012--"; + #pragma pack(push, 1) struct SendList final { diff --git a/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp b/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp index 8900f00239..b191642543 100644 --- a/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp +++ b/Source/Core/Core/IOS/Network/KD/NWC24Config.cpp @@ -246,4 +246,16 @@ void NWC24Config::SetPassword(std::string_view password) std::strncpy(m_data.paswd, password.data(), std::size(m_data.paswd)); m_data.paswd[MAX_PASSWORD_LENGTH - 1] = '\0'; } + +std::string NWC24Config::GetSendURL() const +{ + const size_t size = strnlen(m_data.http_urls[4], MAX_URL_LENGTH); + return {m_data.http_urls[4], size}; +} + +std::string_view NWC24Config::GetPassword() const +{ + const size_t size = strnlen(m_data.paswd, MAX_PASSWORD_LENGTH); + return {m_data.paswd, 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 3f1f7e8e90..9db688ee02 100644 --- a/Source/Core/Core/IOS/Network/KD/NWC24Config.h +++ b/Source/Core/Core/IOS/Network/KD/NWC24Config.h @@ -34,6 +34,8 @@ enum ErrorCode : s32 WC24_ERR_ID_REGISTERED = -36, WC24_ERR_DISABLED = -39, WC24_ERR_ID_NOT_REGISTERED = -44, + WC24_MSG_DAMAGED = -71, + WC24_MSG_TOO_BIG = -72 }; enum class NWC24CreationStage : u32 @@ -72,6 +74,8 @@ public: std::string_view GetMlchkid() const; std::string GetCheckURL() const; + std::string GetSendURL() const; + std::string_view GetPassword() const; NWC24CreationStage CreationStage() const; void SetCreationStage(NWC24CreationStage creation_stage); diff --git a/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp b/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp index 18e2396b38..8bc813a96b 100644 --- a/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp +++ b/Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp @@ -155,7 +155,7 @@ s32 NWC24MakeUserID(u64* nwc24_id, u32 hollywood_id, u16 id_ctr, HardwareModel h NetKDRequestDevice::NetKDRequestDevice(EmulationKernel& ios, const std::string& device_name) : EmulationDevice(ios, device_name), m_config{ios.GetFS()}, m_dl_list{ios.GetFS()}, - m_send_list{ios.GetFS()} + m_send_list{ios.GetFS()}, m_friend_list{ios.GetFS()} { // Enable all NWC24 permissions m_scheduler_buffer[1] = Common::swap32(-1); @@ -275,6 +275,12 @@ void NetKDRequestDevice::SchedulerWorker(const SchedulerEvent event) { LogError(ErrorType::CheckMail, code); } + + code = KDSendMail(); + if (code != NWC24::WC24_OK) + { + LogError(ErrorType::SendMail, code); + } } } @@ -315,6 +321,15 @@ void NetKDRequestDevice::LogError(ErrorType error_type, s32 error_code) case ErrorType::CheckMail: new_code = -(102200 - error_code); break; + case ErrorType::SendMail: + new_code = -(105000 - error_code); + break; + case ErrorType::ReceiveMail: + new_code = -(100300 - error_code); + break; + case ErrorType::CGI: + new_code = -(error_code + 110000); + break; } std::lock_guard lg(m_scheduler_buffer_lock); @@ -498,6 +513,133 @@ NWC24::ErrorCode NetKDRequestDevice::DetermineSubtask(u16 entry_index, return NWC24::WC24_ERR_INVALID_VALUE; } +NWC24::ErrorCode NetKDRequestDevice::KDSendMail() +{ + bool success = false; + Common::ScopeGuard exit_guard([&] { + std::lock_guard lg(m_scheduler_buffer_lock); + if (success) + { + // m_scheduler_buffer[11] contains the amount of times we have sent for mail this IOS + // session. + m_scheduler_buffer[14] = Common::swap32(Common::swap32(m_scheduler_buffer[14]) + 1); + } + m_scheduler_buffer[4] = static_cast(CurrentFunction::None); + + m_send_list.WriteSendList(); + }); + + { + std::lock_guard lg(m_scheduler_buffer_lock); + m_scheduler_buffer[4] = Common::swap32(static_cast(CurrentFunction::Send)); + } + + m_send_list.ReadSendList(); + const std::string auth = + fmt::format("mlid=w{}\r\npasswd={}", m_config.Id(), m_config.GetPassword()); + std::vector multiform = {{"mlid", auth}}; + + std::vector mails = m_send_list.GetMailToSend(); + for (const u32 file_index : mails) + { + const u32 entry_id = m_send_list.GetEntryId(file_index); + const u32 mail_size = m_send_list.GetMailSize(file_index); + if (mail_size > MAX_MAIL_SIZE) + { + WARN_LOG_FMT(IOS_WC24, + "NET_KD_REQ: IOCTL_NWC24_SEND_MAIL_NOW: Mail at index {} was too large to send.", + entry_id); + LogError(ErrorType::SendMail, NWC24::WC24_MSG_TOO_BIG); + + NWC24::ErrorCode res = m_send_list.DeleteMessage(file_index); + if (res != NWC24::WC24_OK) + { + LogError(ErrorType::SendMail, res); + } + mails.erase(std::remove(mails.begin(), mails.end(), file_index), mails.end()); + continue; + } + + std::vector mail_data(mail_size); + NWC24::ErrorCode res = NWC24::ReadFromVFF( + NWC24::Mail::SEND_BOX_PATH, m_send_list.GetMailPath(file_index), m_ios.GetFS(), mail_data); + if (res != NWC24::WC24_OK) + { + ERROR_LOG_FMT(IOS_WC24, "Reading mail at index {} failed with error code {}.", entry_id, + static_cast(res)); + LogError(ErrorType::SendMail, NWC24::WC24_MSG_DAMAGED); + res = m_send_list.DeleteMessage(file_index); + if (res != NWC24::WC24_OK) + { + LogError(ErrorType::SendMail, res); + } + + mails.erase(std::remove(mails.begin(), mails.end(), file_index), mails.end()); + continue; + } + + const std::string mail_str = {mail_data.begin(), mail_data.end()}; + + multiform.push_back({fmt::format("m{}", entry_id), mail_str}); + } + + const Common::HttpRequest::Response response = + m_http.PostMultiform(m_config.GetSendURL(), multiform); + + if (!response) + { + ERROR_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_SEND_MAIL_NOW: Failed to request data at {}.", + m_config.GetSendURL()); + return NWC24::WC24_ERR_SERVER; + } + + // Now check if any mail failed to save to the 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; + } + + // Reverse in order to delete from bottom to top of the send list. + // We do this to ensure that new entries can be written as close to the beginning of the file as + // possible. + for (auto it = mails.rbegin(); it != mails.rend(); ++it) + { + const u32 entry_id = m_send_list.GetEntryId(*it); + Common::ScopeGuard delete_guard([&] { + NWC24::ErrorCode res = m_send_list.DeleteMessage(*it); + if (res != NWC24::WC24_OK) + { + LogError(ErrorType::SendMail, res); + } + }); + + const std::string value = GetValueFromCGIResponse(response_str, fmt::format("cd{}", entry_id)); + + s32 cgi_code{}; + const bool did_parse = TryParse(value, &cgi_code); + if (!did_parse) + { + ERROR_LOG_FMT(IOS_WC24, "Mail server returned invalid CGI response code."); + LogError(ErrorType::CGI, NWC24::WC24_ERR_SERVER); + break; + } + + if (cgi_code != 100) + { + ERROR_LOG_FMT(IOS_WC24, "Mail server failed to save mail at index {}", entry_id); + LogError(ErrorType::CGI, cgi_code); + } + } + + success = true; + return NWC24::WC24_OK; +} + NWC24::ErrorCode NetKDRequestDevice::KDDownload(const u16 entry_index, const std::optional subtask_id) { @@ -622,8 +764,8 @@ NWC24::ErrorCode NetKDRequestDevice::KDDownload(const u16 entry_index, } } - NWC24::ErrorCode reply = IOS::HLE::NWC24::OpenVFF(m_dl_list.GetVFFPath(entry_index), content_name, - m_ios.GetFS(), file_data); + NWC24::ErrorCode reply = IOS::HLE::NWC24::WriteToVFF(m_dl_list.GetVFFPath(entry_index), + content_name, m_ios.GetFS(), file_data); if (reply != NWC24::WC24_OK) { @@ -657,6 +799,13 @@ IPCReply NetKDRequestDevice::HandleNWC24CheckMailNow(const IOCtlRequest& request return IPCReply(IPC_SUCCESS); } +IPCReply NetKDRequestDevice::HandleNWC24SendMailNow(const IOCtlRequest& request) +{ + const NWC24::ErrorCode reply = KDSendMail(); + WriteReturnValue(reply, request.buffer_out); + return IPCReply(IPC_SUCCESS); +} + IPCReply NetKDRequestDevice::HandleNWC24DownloadNowEx(const IOCtlRequest& request) { if (m_dl_list.IsDisabled() || !m_dl_list.ReadDlList()) @@ -981,6 +1130,9 @@ std::optional NetKDRequestDevice::IOCtl(const IOCtlRequest& request) case IOCTL_NWC24_DOWNLOAD_NOW_EX: return LaunchAsyncTask(&NetKDRequestDevice::HandleNWC24DownloadNowEx, request); + case IOCTL_NWC24_SEND_MAIL_NOW: + return LaunchAsyncTask(&NetKDRequestDevice::HandleNWC24SendMailNow, request); + case IOCTL_NWC24_REQUEST_SHUTDOWN: { if (request.buffer_in == 0 || request.buffer_in % 4 != 0 || request.buffer_in_size < 8 || diff --git a/Source/Core/Core/IOS/Network/KD/NetKDRequest.h b/Source/Core/Core/IOS/Network/KD/NetKDRequest.h index 10201eab0d..0fcd43f52d 100644 --- a/Source/Core/Core/IOS/Network/KD/NetKDRequest.h +++ b/Source/Core/Core/IOS/Network/KD/NetKDRequest.h @@ -13,6 +13,7 @@ #include "Common/HttpRequest.h" #include "Common/WorkQueueThread.h" #include "Core/IOS/Device.h" +#include "Core/IOS/Network/KD/Mail/WC24FriendList.h" #include "Core/IOS/Network/KD/Mail/WC24Send.h" #include "Core/IOS/Network/KD/NWC24Config.h" #include "Core/IOS/Network/KD/NWC24DL.h" @@ -72,6 +73,9 @@ private: Client, Server, CheckMail, + SendMail, + ReceiveMail, + CGI, }; enum class SchedulerEvent @@ -80,14 +84,18 @@ private: Download, }; + IPCReply HandleNWC24SendMailNow(const IOCtlRequest& request); NWC24::ErrorCode KDCheckMail(u32* mail_flag, u32* interval); IPCReply HandleRequestRegisterUserId(const IOCtlRequest& request); + NWC24::ErrorCode KDSendMail(); + void LogError(ErrorType error_type, s32 error_code); void SchedulerTimer(); void SchedulerWorker(SchedulerEvent event); NWC24::ErrorCode DetermineDownloadTask(u16* entry_index, std::optional* subtask_id) const; NWC24::ErrorCode DetermineSubtask(u16 entry_index, std::optional* subtask_id) const; + static constexpr u32 MAX_MAIL_SIZE = 208952; 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, @@ -98,6 +106,7 @@ private: NWC24::NWC24Config m_config; NWC24::NWC24Dl m_dl_list; NWC24::Mail::WC24SendList m_send_list; + NWC24::Mail::WC24FriendList m_friend_list; Common::WorkQueueThread m_work_queue; Common::WorkQueueThread> m_scheduler_work_queue; std::mutex m_async_reply_lock; diff --git a/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.cpp b/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.cpp index c616199105..01d041644d 100644 --- a/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.cpp +++ b/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.cpp @@ -237,6 +237,48 @@ static ErrorCode WriteFile(const std::string& filename, const std::vector& t return WC24_OK; } +static ErrorCode ReadFile(const std::string& filename, std::vector& out) +{ + FIL src{}; + const auto open_error_code = f_open(&src, filename.c_str(), FA_READ); + if (open_error_code != FR_OK) + { + ERROR_LOG_FMT(IOS_WC24, "Failed to open file {} in VFF", filename); + return WC24_ERR_FILE_OPEN; + } + + Common::ScopeGuard vff_close_guard{[&] { f_close(&src); }}; + + u32 size = static_cast(out.size()); + u32 read_size{}; + const auto read_error_code = f_read(&src, out.data(), size, &read_size); + if (read_error_code != FR_OK) + { + ERROR_LOG_FMT(IOS_WC24, "Failed to read file {} in VFF: {}", filename, + static_cast(read_error_code)); + return WC24_ERR_FILE_READ; + } + + if (read_size != size) + { + ERROR_LOG_FMT(IOS_WC24, "Failed to read bytes of file {} to VFF ({} != {})", filename, + read_size, size); + return WC24_ERR_FILE_READ; + } + + // As prior operations did not fail, dismiss the guard and handle a potential error with f_close. + vff_close_guard.Dismiss(); + + const auto close_error_code = f_close(&src); + if (close_error_code != FR_OK) + { + ERROR_LOG_FMT(IOS_WC24, "Failed to close file {} in VFF", filename); + return WC24_ERR_FILE_CLOSE; + } + + return WC24_OK; +} + namespace { class VffFatFsCallbacks : public Common::FatFsCallbacks @@ -258,8 +300,8 @@ public: }; } // namespace -ErrorCode OpenVFF(const std::string& path, const std::string& filename, - const std::shared_ptr& fs, const std::vector& data) +ErrorCode WriteToVFF(const std::string& path, const std::string& filename, + const std::shared_ptr& fs, const std::vector& data) { VffFatFsCallbacks callbacks; ErrorCode return_value; @@ -286,6 +328,8 @@ ErrorCode OpenVFF(const std::string& path, const std::string& filename, return; } + Common::ScopeGuard unmount_guard{[] { f_unmount(""); }}; + const FRESULT vff_mount_error_code = vff_mount(callbacks.m_vff, &fatfs); if (vff_mount_error_code != FR_OK) { @@ -295,8 +339,6 @@ ErrorCode OpenVFF(const std::string& path, const std::string& filename, return; } - Common::ScopeGuard unmount_guard{[] { f_unmount(""); }}; - const auto write_error_code = WriteFile(filename, data); if (write_error_code != WC24_OK) { @@ -312,4 +354,108 @@ ErrorCode OpenVFF(const std::string& path, const std::string& filename, return return_value; } + +ErrorCode ReadFromVFF(const std::string& path, const std::string& filename, + const std::shared_ptr& fs, std::vector& out) +{ + VffFatFsCallbacks callbacks; + ErrorCode return_value; + Common::RunInFatFsContext(callbacks, [&]() { + auto temp = fs->OpenFile(PID_KD, PID_KD, path, FS::Mode::ReadWrite); + if (!temp) + { + ERROR_LOG_FMT(IOS_WC24, "Failed to open VFF at: {}", path); + return_value = WC24_ERR_NOT_FOUND; + return; + } + + callbacks.m_vff = &*temp; + + FATFS fatfs{}; + const FRESULT fatfs_mount_error_code = f_mount(&fatfs, "", 0); + if (fatfs_mount_error_code != FR_OK) + { + // The VFF is most likely broken. + ERROR_LOG_FMT(IOS_WC24, "Failed to mount VFF at: {}", path); + return_value = WC24_ERR_BROKEN; + return; + } + + Common::ScopeGuard unmount_guard{[] { f_unmount(""); }}; + + const FRESULT vff_mount_error_code = vff_mount(callbacks.m_vff, &fatfs); + if (vff_mount_error_code != FR_OK) + { + // The VFF is most likely broken. + ERROR_LOG_FMT(IOS_WC24, "Failed to mount VFF at: {}", path); + return_value = WC24_ERR_BROKEN; + return; + } + + const ErrorCode read_error_code = ReadFile(filename, out); + if (read_error_code != WC24_OK) + { + return_value = read_error_code; + return; + } + + return_value = WC24_OK; + return; + }); + + return return_value; +} + +ErrorCode DeleteFileFromVFF(const std::string& path, const std::string& filename, + const std::shared_ptr& fs) +{ + VffFatFsCallbacks callbacks; + ErrorCode return_value; + Common::RunInFatFsContext(callbacks, [&]() { + auto temp = fs->OpenFile(PID_KD, PID_KD, path, FS::Mode::ReadWrite); + if (!temp) + { + ERROR_LOG_FMT(IOS_WC24, "Failed to open VFF at: {}", path); + return_value = WC24_ERR_NOT_FOUND; + return; + } + + callbacks.m_vff = &*temp; + + FATFS fatfs{}; + const FRESULT fatfs_mount_error_code = f_mount(&fatfs, "", 0); + if (fatfs_mount_error_code != FR_OK) + { + // The VFF is most likely broken. + ERROR_LOG_FMT(IOS_WC24, "Failed to mount VFF at: {}", path); + return_value = WC24_ERR_BROKEN; + return; + } + + Common::ScopeGuard unmount_guard{[] { f_unmount(""); }}; + + const FRESULT vff_mount_error_code = vff_mount(callbacks.m_vff, &fatfs); + if (vff_mount_error_code != FR_OK) + { + // The VFF is most likely broken. + ERROR_LOG_FMT(IOS_WC24, "Failed to mount VFF at: {}", path); + return_value = WC24_ERR_BROKEN; + return; + } + + const FRESULT unlink_code = f_unlink(filename.c_str()); + if (unlink_code != FR_OK) + { + ERROR_LOG_FMT(IOS_WC24, "Failed to delete file {} in VFF at: {} Code: {}", filename, path, + static_cast(unlink_code)); + return_value = WC24_ERR_BROKEN; + return; + } + + return_value = WC24_OK; + return; + }); + + return return_value; +} } // namespace IOS::HLE::NWC24 diff --git a/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.h b/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.h index ca3dc4c5a8..5decfb90e4 100644 --- a/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.h +++ b/Source/Core/Core/IOS/Network/KD/VFF/VFFUtil.h @@ -22,8 +22,12 @@ namespace NWC24 constexpr u16 SECTOR_SIZE = 512; constexpr u16 VF_LITTLE_ENDIAN = 0xFFFE; constexpr u16 VF_BIG_ENDIAN = 0xFEFF; -ErrorCode OpenVFF(const std::string& path, const std::string& filename, - const std::shared_ptr& fs, const std::vector& data); +ErrorCode WriteToVFF(const std::string& path, const std::string& filename, + const std::shared_ptr& fs, const std::vector& data); +ErrorCode ReadFromVFF(const std::string& path, const std::string& filename, + const std::shared_ptr& fs, std::vector& out); +ErrorCode DeleteFileFromVFF(const std::string& path, const std::string& filename, + const std::shared_ptr& fs); #pragma pack(push, 1) struct VFFHeader final diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index f111b6f624..cc564bbafd 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -366,6 +366,7 @@ + @@ -1017,6 +1018,7 @@ +