6 changes: 5 additions & 1 deletion Source/Core/Core/IOS/Network/KD/NWC24Config.h
Expand Up @@ -69,6 +69,9 @@ class NWC24Config final
u32 Checksum() const;
void SetChecksum(u32 checksum);

std::string_view GetMlchkid() const;
std::string GetCheckURL() const;

NWC24CreationStage CreationStage() const;
void SetCreationStage(NWC24CreationStage creation_stage);

Expand All @@ -92,6 +95,7 @@ class NWC24Config final
MAX_URL_LENGTH = 0x80,
MAX_EMAIL_LENGTH = 0x40,
MAX_PASSWORD_LENGTH = 0x20,
MAX_MLCHKID_LENGTH = 0x24,
};

#pragma pack(push, 1)
Expand All @@ -104,7 +108,7 @@ class NWC24Config final
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;
Expand Down
237 changes: 222 additions & 15 deletions Source/Core/Core/IOS/Network/KD/NetKDRequest.cpp
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand All @@ -161,13 +165,29 @@ 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<void()> task) { task(); });

m_scheduler_timer_thread = std::thread([this] { SchedulerTimer(); });
}

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()
Expand All @@ -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<std::string> raw_fields = SplitString(response, '\n');
for (const std::string& field : raw_fields)
{
const std::vector<std::string> 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{};
Expand All @@ -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);
Expand All @@ -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<u32>(CurrentFunction::None);
});

{
std::lock_guard lg(m_scheduler_buffer_lock);
m_scheduler_buffer[4] = Common::swap32(static_cast<u32>(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<u8, 20> hashed{};
Common::HMAC::HMACWithSHA1(
MAIL_CHECK_KEY,
std::span<const u8>(reinterpret_cast<const u8*>(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<u8> subtask_id)
{
Expand Down Expand Up @@ -308,6 +497,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();
Expand Down Expand Up @@ -446,7 +650,7 @@ std::optional<IPCReply> 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;
Expand All @@ -467,19 +671,19 @@ std::optional<IPCReply> 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);
}
Expand All @@ -489,16 +693,16 @@ std::optional<IPCReply> 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:
Expand All @@ -514,8 +718,8 @@ std::optional<IPCReply> 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;
Expand All @@ -525,6 +729,9 @@ std::optional<IPCReply> 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);

Expand Down
40 changes: 39 additions & 1 deletion Source/Core/Core/IOS/Network/KD/NetKDRequest.h
Expand Up @@ -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"

Expand All @@ -26,6 +28,7 @@ class NetKDRequestDevice : public EmulationDevice
NetKDRequestDevice(EmulationKernel& ios, const std::string& device_name);
IPCReply HandleNWC24DownloadNowEx(const IOCtlRequest& request);
NWC24::ErrorCode KDDownload(const u16 entry_index, const std::optional<u8> subtask_id);
IPCReply HandleNWC24CheckMailNow(const IOCtlRequest& request);
~NetKDRequestDevice() override;

std::optional<IPCReply> IOCtl(const IOCtlRequest& request) override;
Expand All @@ -51,25 +54,60 @@ class NetKDRequestDevice : public EmulationDevice
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,
};

enum class SchedulerEvent
{
Mail,
Download,
};

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<u8, 20> 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 config;
NWC24::NWC24Config m_config;
NWC24::NWC24Dl m_dl_list;
NWC24::Mail::WC24SendList m_send_list;
Common::WorkQueueThread<AsyncTask> m_work_queue;
Common::WorkQueueThread<std::function<void()>> m_scheduler_work_queue;
std::mutex m_async_reply_lock;
std::mutex m_scheduler_buffer_lock;
std::queue<AsyncReply> m_async_replies;
u32 m_error_count = 0;
std::array<u32, 256> 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
5 changes: 5 additions & 0 deletions Source/Core/DolphinLib.props
Expand Up @@ -35,6 +35,7 @@
<ClInclude Include="Common\Crypto\AES.h" />
<ClInclude Include="Common\Crypto\bn.h" />
<ClInclude Include="Common\Crypto\ec.h" />
<ClInclude Include="Common\Crypto\HMAC.h" />
<ClInclude Include="Common\Crypto\SHA1.h" />
<ClInclude Include="Common\Debug\MemoryPatches.h" />
<ClInclude Include="Common\Debug\Threads.h" />
Expand Down Expand Up @@ -362,6 +363,8 @@
<ClInclude Include="Core\IOS\Network\KD\NWC24DL.h" />
<ClInclude Include="Core\IOS\Network\KD\VFF\VFFUtil.h" />
<ClInclude Include="Core\IOS\Network\KD\WC24File.h" />
<ClInclude Include="Core\IOS\Network\KD\Mail\MailCommon.h" />
<ClInclude Include="Core\IOS\Network\KD\Mail\WC24Send.h" />
<ClInclude Include="Core\IOS\Network\MACUtils.h" />
<ClInclude Include="Core\IOS\Network\NCD\Manage.h" />
<ClInclude Include="Core\IOS\Network\NCD\WiiNetConfig.h" />
Expand Down Expand Up @@ -762,6 +765,7 @@
<ClCompile Include="Common\Crypto\AES.cpp" />
<ClCompile Include="Common\Crypto\bn.cpp" />
<ClCompile Include="Common\Crypto\ec.cpp" />
<ClCompile Include="Common\Crypto\HMAC.cpp" />
<ClCompile Include="Common\Crypto\SHA1.cpp" />
<ClCompile Include="Common\Debug\MemoryPatches.cpp" />
<ClCompile Include="Common\Debug\Watches.cpp" />
Expand Down Expand Up @@ -1005,6 +1009,7 @@
<ClCompile Include="Core\IOS\Network\KD\NWC24Config.cpp" />
<ClCompile Include="Core\IOS\Network\KD\NWC24DL.cpp" />
<ClCompile Include="Core\IOS\Network\KD\VFF\VFFUtil.cpp" />
<ClCompile Include="Core\IOS\Network\KD\Mail\WC24Send.cpp" />
<ClCompile Include="Core\IOS\Network\MACUtils.cpp" />
<ClCompile Include="Core\IOS\Network\NCD\Manage.cpp" />
<ClCompile Include="Core\IOS\Network\NCD\WiiNetConfig.cpp" />
Expand Down