@@ -9,6 +9,7 @@
#include <string_view>
#include <utility>

#include "Common/BitUtils.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
@@ -19,6 +20,7 @@
#include "Core/CommonTitles.h"
#include "Core/HW/Memmap.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/IOS/Network/KD/VFF/VFFUtil.h"
#include "Core/IOS/Network/Socket.h"
#include "Core/IOS/Uids.h"

@@ -145,15 +147,149 @@ s32 NWC24MakeUserID(u64* nwc24_id, u32 hollywood_id, u16 id_ctr, HardwareModel h
} // Anonymous namespace

NetKDRequestDevice::NetKDRequestDevice(Kernel& ios, const std::string& device_name)
: Device(ios, device_name), config{ios.GetFS()}
: Device(ios, device_name), config{ios.GetFS()}, m_dl_list{ios.GetFS()}
{
m_work_queue.Reset([this](AsyncTask task) {
const IPCReply reply = task.handler();
{
std::lock_guard lg(m_async_reply_lock);
m_async_replies.emplace(AsyncReply{task.request, reply.return_value});
}
});
}

NetKDRequestDevice::~NetKDRequestDevice()
{
WiiSockMan::GetInstance().Clean();
}

void NetKDRequestDevice::Update()
{
{
std::lock_guard lg(m_async_reply_lock);
while (!m_async_replies.empty())
{
const auto& reply = m_async_replies.front();
GetIOS()->EnqueueIPCReply(reply.request, reply.return_value);
m_async_replies.pop();
}
}
}

NWC24::ErrorCode NetKDRequestDevice::KDDownload(const u16 entry_index,
const std::optional<u8> subtask_id)
{
std::vector<u8> file_data;

// Content metadata
const std::string content_name = m_dl_list.GetVFFContentName(entry_index, subtask_id);
const std::string url = m_dl_list.GetDownloadURL(entry_index, subtask_id);

INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_DOWNLOAD_NOW_EX - NI - URL: {}", url);

INFO_LOG_FMT(IOS_WC24, "NET_KD_REQ: IOCTL_NWC24_DOWNLOAD_NOW_EX - NI - Name: {}", content_name);

const Common::HttpRequest::Response response = m_http.Get(url);

if (!response)
{
ERROR_LOG_FMT(IOS_WC24, "Failed to request data at {}", url);
return NWC24::WC24_ERR_SERVER;
}

// Check if the filesize is smaller than the header size.
if (response->size() < sizeof(NWC24::WC24File))
{
ERROR_LOG_FMT(IOS_WC24, "File at {} is too small to be a valid file.", url);
return NWC24::WC24_ERR_BROKEN;
}

// Now we read the file
NWC24::WC24File wc24File;
std::memcpy(&wc24File, response->data(), sizeof(NWC24::WC24File));

std::vector<u8> temp_buffer(response->begin() + 320, response->end());

if (m_dl_list.IsEncrypted(entry_index))
{
NWC24::WC24PubkMod pubkMod = m_dl_list.GetWC24PubkMod(entry_index);

file_data = std::vector<u8>(response->size() - 320);

Common::AES::CryptOFB(pubkMod.aes_key, wc24File.iv, wc24File.iv, temp_buffer.data(),
file_data.data(), temp_buffer.size());
}
else
{
file_data = std::move(temp_buffer);
}

NWC24::ErrorCode reply = IOS::HLE::NWC24::OpenVFF(m_dl_list.GetVFFPath(entry_index), content_name,
m_ios.GetFS(), file_data);

return reply;
}

IPCReply NetKDRequestDevice::HandleNWC24DownloadNowEx(const IOCtlRequest& request)
{
const u32 flags = Memory::Read_U32(request.buffer_in);
// Nintendo converts the entry ID between a u32 and u16
// several times, presumably for alignment purposes.
// We'll skip past buffer_in+4 and keep the entry index as a u16.
const u16 entry_index = Memory::Read_U16(request.buffer_in + 6);
const u32 subtask_bitmask = Memory::Read_U32(request.buffer_in + 8);

INFO_LOG_FMT(IOS_WC24,
"NET_KD_REQ: IOCTL_NWC24_DOWNLOAD_NOW_EX - NI - flags: {}, index: {}, bitmask: {}",
flags, entry_index, subtask_bitmask);

if (entry_index >= NWC24::NWC24Dl::MAX_ENTRIES)
{
ERROR_LOG_FMT(IOS_WC24, "NET_KD_REQ: Entry index out of range.");
WriteReturnValue(NWC24::WC24_ERR_BROKEN, request.buffer_out);
return IPCReply(NWC24::WC24_ERR_BROKEN);
}

if (!m_dl_list.DoesEntryExist(entry_index))
{
ERROR_LOG_FMT(IOS_WC24, "NET_KD_REQ: Requested entry does not exist in download list!");
WriteReturnValue(NWC24::WC24_ERR_NOT_FOUND, request.buffer_out);
return IPCReply(NWC24::WC24_ERR_NOT_FOUND);
}

// While in theory reply will always get initialized by KDDownload, things happen.
// Returning NWC24::WC24_ERR_BROKEN or anything that isn't OK will prompt the channel to fix the
// entry's data.
NWC24::ErrorCode reply = NWC24::WC24_ERR_BROKEN;

// Determine if we have subtasks to handle
if (Common::ExtractBit(flags, 2))
{
for (u8 subtask_id = 0; subtask_id < 32; subtask_id++)
{
// Check if we are done
if (!Common::ExtractBit(subtask_bitmask, subtask_id))
{
break;
}

reply = KDDownload(entry_index, subtask_id);
if (reply != NWC24::WC24_OK)
{
// An error has occurred, break out and return error.
break;
}
}
}
else
{
reply = KDDownload(entry_index, std::nullopt);
}

WriteReturnValue(reply, request.buffer_out);
return IPCReply(reply);
}

std::optional<IPCReply> NetKDRequestDevice::IOCtl(const IOCtlRequest& request)
{
enum : u32
@@ -288,6 +424,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_DOWNLOAD_NOW_EX:
return LaunchAsyncTask(&NetKDRequestDevice::HandleNWC24DownloadNowEx, request);

case IOCTL_NWC24_REQUEST_SHUTDOWN:
{
if (request.buffer_in == 0 || request.buffer_in % 4 != 0 || request.buffer_in_size < 8 ||
@@ -3,25 +3,61 @@

#pragma once

#include <queue>
#include <string>

#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
#include "Common/HttpRequest.h"
#include "Common/WorkQueueThread.h"
#include "Core/IOS/Device.h"
#include "Core/IOS/Network/KD/NWC24Config.h"
#include "Core/IOS/Network/KD/NWC24DL.h"

namespace IOS::HLE
{
constexpr const char DL_CNT_PATH[] = "/" WII_WC24CONF_DIR "/dlcnt.bin";

// KD is the IOS module responsible for implementing WiiConnect24 functionality.
// It can perform HTTPS downloads, send and receive mail via SMTP, and execute a
// JavaScript-like language while the Wii is in standby mode.
class NetKDRequestDevice : public Device
{
public:
NetKDRequestDevice(Kernel& ios, const std::string& device_name);
IPCReply HandleNWC24DownloadNowEx(const IOCtlRequest& request);
NWC24::ErrorCode KDDownload(const u16 entry_index, const std::optional<u8> subtask_id);
~NetKDRequestDevice() override;

std::optional<IPCReply> IOCtl(const IOCtlRequest& request) override;
void Update() override;

private:
struct AsyncTask
{
IOS::HLE::Request request;
std::function<IPCReply()> handler;
};

struct AsyncReply
{
IOS::HLE::Request request;
s32 return_value;
};

template <typename Method, typename Request>
std::optional<IPCReply> LaunchAsyncTask(Method method, const Request& request)
{
m_work_queue.EmplaceItem(AsyncTask{request, std::bind(method, this, request)});
return std::nullopt;
}

NWC24::NWC24Config config;
NWC24::NWC24Dl m_dl_list;
Common::WorkQueueThread<AsyncTask> m_work_queue;
std::mutex m_async_reply_lock;
std::queue<AsyncReply> m_async_replies;
// TODO: Maybe move away from Common::HttpRequest?
Common::HttpRequest m_http{std::chrono::minutes{1}};
};
} // namespace IOS::HLE
@@ -0,0 +1,308 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "Core/IOS/Network/KD/VFF/VFFUtil.h"

#include <algorithm>
#include <cmath>
#include <vector>

#include <fmt/format.h>

// Does not compile if diskio.h is included first.
// clang-format off
#include "ff.h"
#include "diskio.h"
// clang-format on

#include "Common/Align.h"
#include "Common/FatFsUtil.h"
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/Swap.h"

#include "Core/IOS/Uids.h"

static DRESULT read_vff_header(IOS::HLE::FS::FileHandle* vff, FATFS* fs)
{
struct IOS::HLE::NWC24::VFFHeader header;
if (!vff->Read(&header, 1))
{
ERROR_LOG_FMT(IOS_WC24, "Failed to read VFF header.");
return RES_ERROR;
}

u16 cluster_size = 0;
u16 cluster_count = 0;

switch (Common::swap16(header.endianness))
{
case IOS::HLE::NWC24::VF_BIG_ENDIAN:
cluster_size = Common::swap16(header.cluster_size) * 16;
cluster_count = Common::swap32(header.volume_size) / cluster_size;
break;
case IOS::HLE::NWC24::VF_LITTLE_ENDIAN:
// TODO: Actually implement.
// Another option is to just delete these VFFs and let the current channel create a Big Endian
// one.
return RES_ERROR;
}

if (cluster_count < 4085)
{
fs->fs_type = FS_FAT12;

u32 table_size = ((cluster_count + 1) / 2) * 3;
table_size = Common::AlignUp(table_size, cluster_size);

// Fsize is the full table size divided by 512 (Cluster size).
fs->fsize = table_size / 512;
}
else if (cluster_count < 65525)
{
fs->fs_type = FS_FAT16;

u32 table_size = cluster_count * 2;
table_size = Common::AlignUp(table_size, cluster_size);
fs->fsize = table_size / 512;
}
else
{
ERROR_LOG_FMT(IOS_WC24, "VFF not FAT12 or 16! Cluster size: {}", cluster_size);
return RES_ERROR;
}

fs->n_fats = 2;
fs->csize = 1;

// Root directory entry is 4096 bytes long, with each entry being 32 bytes. 4096 / 32 = 128
fs->n_rootdir = 128;

u32 sysect = 1 + (fs->fsize * 2) + fs->n_rootdir / (512 / 32);

// cluster_count is the total count whereas this is the actual amount of clusters we can use
u32 actual_cluster_count = cluster_count - sysect;

fs->n_fatent = actual_cluster_count + 2;
fs->volbase = 0;
fs->fatbase = 1;
fs->database = sysect;
// Root directory entry
fs->dirbase = fs->fatbase + fs->fsize * 2;

// Initialize cluster allocation information
fs->last_clst = fs->free_clst = 0xFFFFFFFF;
fs->fsi_flag = 0x80;

fs->id = 0;
fs->cdir = 0;

return RES_OK;
}

static FRESULT vff_mount(IOS::HLE::FS::FileHandle* vff, FATFS* fs)
{
fs->fs_type = 0; // Clear the filesystem object
fs->pdrv = 0; // Volume hosting physical drive

DRESULT ret = read_vff_header(vff, fs);
if (ret != RES_OK)
return FR_DISK_ERR;

return FR_OK;
}

static DRESULT vff_read(IOS::HLE::FS::FileHandle* vff, BYTE pdrv, BYTE* buff, LBA_t sector,
UINT count)
{
// We cannot read or write data to the 0th sector in a VFF.
if (sector == 0)
{
ERROR_LOG_FMT(IOS_WC24, "Attempted to read the 0th sector in the VFF: Invalid VFF?");
return RES_ERROR;
}

const u64 offset = static_cast<u64>(sector) * IOS::HLE::NWC24::SECTOR_SIZE - 480;
if (!vff->Seek(offset, IOS::HLE::FS::SeekMode::Set))
{
ERROR_LOG_FMT(IOS_WC24, "VFF seek failed (offset={})", offset);
return RES_ERROR;
}

const size_t size = static_cast<size_t>(count) * IOS::HLE::NWC24::SECTOR_SIZE;
if (!vff->Read(buff, size))
{
ERROR_LOG_FMT(IOS_WC24, "VFF read failed (offset={}, size={})", offset, size);
return RES_ERROR;
}

return RES_OK;
}

static DRESULT vff_write(IOS::HLE::FS::FileHandle* vff, BYTE pdrv, const BYTE* buff, LBA_t sector,
UINT count)
{
if (sector == 0)
{
ERROR_LOG_FMT(IOS_WC24, "Attempted to write to the 0th sector in the VFF: Invalid VFF?");
return RES_ERROR;
}

const u64 offset = static_cast<u64>(sector) * IOS::HLE::NWC24::SECTOR_SIZE - 480;
if (!vff->Seek(offset, IOS::HLE::FS::SeekMode::Set))
{
ERROR_LOG_FMT(IOS_WC24, "VFF seek failed (offset={})", offset);
return RES_ERROR;
}

const size_t size = static_cast<size_t>(count) * IOS::HLE::NWC24::SECTOR_SIZE;
const auto res = vff->Write(buff, size);
if (!res)
{
ERROR_LOG_FMT(IOS_WC24, "VFF write failed (offset={}, size={})", offset, size);
return RES_ERROR;
}

return RES_OK;
}

static DRESULT vff_ioctl(IOS::HLE::FS::FileHandle* vff, BYTE pdrv, BYTE cmd, void* buff)
{
switch (cmd)
{
case CTRL_SYNC:
return RES_OK;
case GET_SECTOR_COUNT:
*reinterpret_cast<LBA_t*>(buff) = vff->GetStatus()->size / IOS::HLE::NWC24::SECTOR_SIZE;
return RES_OK;
default:
WARN_LOG_FMT(IOS_WC24, "Unexpected FAT ioctl {}", cmd);
return RES_OK;
}
}

namespace IOS::HLE::NWC24
{
static ErrorCode WriteFile(const std::string& filename, const std::vector<u8>& tmp_buffer)
{
FIL dst;
const auto open_error_code = f_open(&dst, filename.c_str(), FA_CREATE_ALWAYS | FA_WRITE);
if (open_error_code != FR_OK)
{
ERROR_LOG_FMT(IOS_WC24, "Failed to open file {} in VFF", filename);
return WC24_ERR_FILE_OPEN;
}

size_t size = tmp_buffer.size();
size_t offset = 0;
while (size > 0)
{
constexpr size_t MAX_CHUNK_SIZE = 32768;
u32 chunk_size = static_cast<u32>(std::min(size, MAX_CHUNK_SIZE));

u32 written_size;
const auto write_error_code =
f_write(&dst, tmp_buffer.data() + offset, chunk_size, &written_size);
if (write_error_code != FR_OK)
{
ERROR_LOG_FMT(IOS_WC24, "Failed to write file {} to VFF {}", filename, write_error_code);
return WC24_ERR_FILE_WRITE;
}

if (written_size != chunk_size)
{
ERROR_LOG_FMT(IOS_WC24, "Failed to write bytes of file {} to VFF ({} != {})", filename,
written_size, chunk_size);
return WC24_ERR_FILE_WRITE;
}

size -= chunk_size;
offset += chunk_size;
}

const auto close_error_code = f_close(&dst);
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
{
public:
int DiskRead(u8 pdrv, u8* buff, u32 sector, unsigned int count) override
{
return vff_read(m_vff, pdrv, buff, sector, count);
}

int DiskWrite(u8 pdrv, const u8* buff, u32 sector, unsigned int count) override
{
return vff_write(m_vff, pdrv, buff, sector, count);
}

int DiskIOCtl(u8 pdrv, u8 cmd, void* buff) override { return vff_ioctl(m_vff, pdrv, cmd, buff); }

IOS::HLE::FS::FileHandle* m_vff;
};
} // namespace

ErrorCode OpenVFF(const std::string& path, const std::string& filename,
const std::shared_ptr<FS::FileSystem>& fs, const std::vector<u8>& data)
{
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;

Common::ScopeGuard vff_delete_guard{[&] { fs->Delete(PID_KD, PID_KD, path); }};

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;
}

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;
}

Common::ScopeGuard unmount_guard{[] { f_unmount(""); }};

const auto write_error_code = WriteFile(filename, data);
if (write_error_code != WC24_OK)
{
return_value = write_error_code;
return;
}

vff_delete_guard.Dismiss();

return_value = WC24_OK;
return;
});

return return_value;
}
} // namespace IOS::HLE::NWC24
@@ -0,0 +1,43 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <memory>
#include <string>
#include <vector>

#include "Common/CommonTypes.h"
#include "Core/IOS/FS/FileSystem.h"
#include "Core/IOS/Network/KD/NWC24Config.h"

namespace IOS::HLE
{
namespace FS
{
class FileSystem;
}
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::FileSystem>& fs, const std::vector<u8>& data);

#pragma pack(push, 1)
struct VFFHeader final
{
u8 magic[4];
u16 endianness;
u16 unknown_marker;
u32 volume_size;
u16 cluster_size;
u16 empty;
u16 unknown;
u8 padding[14];
};
static_assert(sizeof(VFFHeader) == 32);
#pragma pack(pop)
} // namespace NWC24
} // namespace IOS::HLE
@@ -0,0 +1,31 @@
// Copyright 2022 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include "Common/CommonTypes.h"

namespace IOS::HLE::NWC24
{
#pragma pack(push, 1)
struct WC24File final
{
char magic[4];
u32 version;
u32 filler;
u8 crypt_type;
u8 padding[3];
u8 reserved[32];
u8 iv[16];
u8 rsa_signature[256];
};

struct WC24PubkMod final
{
u8 rsa_public[256];
u8 rsa_reserved[256];
u8 aes_key[16];
u8 aes_reserved[16];
};
#pragma pack(pop)
} // namespace IOS::HLE::NWC24
@@ -354,6 +354,9 @@
<ClInclude Include="Core\IOS\Network\KD\NetKDRequest.h" />
<ClInclude Include="Core\IOS\Network\KD\NetKDTime.h" />
<ClInclude Include="Core\IOS\Network\KD\NWC24Config.h" />
<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\MACUtils.h" />
<ClInclude Include="Core\IOS\Network\NCD\Manage.h" />
<ClInclude Include="Core\IOS\Network\NCD\WiiNetConfig.h" />
@@ -980,6 +983,8 @@
<ClCompile Include="Core\IOS\Network\KD\NetKDRequest.cpp" />
<ClCompile Include="Core\IOS\Network\KD\NetKDTime.cpp" />
<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\MACUtils.cpp" />
<ClCompile Include="Core\IOS\Network\NCD\Manage.cpp" />
<ClCompile Include="Core\IOS\Network\NCD\WiiNetConfig.cpp" />