mirror of
https://github.com/cemu-project/Cemu.git
synced 2025-12-12 01:36:58 +00:00
614 lines
20 KiB
C++
614 lines
20 KiB
C++
#include "Common/precompiled.h"
|
|
#include "Cemu/ncrypto/ncrypto.h"
|
|
#include "napi.h"
|
|
#include "napi_helper.h"
|
|
|
|
#include "curl/curl.h"
|
|
#include "pugixml.hpp"
|
|
#include "Cafe/IOSU/legacy/iosu_crypto.h"
|
|
|
|
#include "config/ActiveSettings.h"
|
|
#include "util/helpers/StringHelpers.h"
|
|
#include "util/highresolutiontimer/HighResolutionTimer.h"
|
|
#include "config/LaunchSettings.h"
|
|
|
|
namespace NAPI
|
|
{
|
|
struct ACTOauthToken : public _NAPI_CommonResultACT
|
|
{
|
|
std::string token;
|
|
std::string refreshToken;
|
|
};
|
|
|
|
bool _parseActResponse(CurlRequestHelper& requestHelper, _NAPI_CommonResultACT& result, pugi::xml_document& doc)
|
|
{
|
|
if (!doc.load_buffer(requestHelper.getReceivedData().data(), requestHelper.getReceivedData().size()))
|
|
{
|
|
cemuLog_log(LogType::Force, fmt::format("Invalid XML in account service response"));
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
return false;
|
|
}
|
|
// check for error codes
|
|
pugi::xml_node errors = doc.child("errors");
|
|
if (errors)
|
|
{
|
|
pugi::xml_node error = errors.child("error");
|
|
if (error)
|
|
{
|
|
std::string_view errorCodeStr = error.child_value("code");
|
|
std::string_view errorCodeMsg = error.child_value("message");
|
|
sint32 errorCode = StringHelpers::ToInt(errorCodeStr);
|
|
if (errorCode == 0)
|
|
{
|
|
cemuLog_force("Account response with unexpected error code 0");
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
}
|
|
else
|
|
{
|
|
result.apiError = NAPI_RESULT::SERVICE_ERROR;
|
|
result.serviceError = (ACT_ERROR_CODE)errorCode;
|
|
cemuLog_force("Account response with error code {}", errorCode);
|
|
if(!errorCodeMsg.empty())
|
|
cemuLog_force("Message from server: {}", errorCodeMsg);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void _ACTSetCommonHeaderParameters(CurlRequestHelper& req, AuthInfo& authInfo)
|
|
{
|
|
req.addHeaderField("X-Nintendo-Platform-ID", "1");
|
|
req.addHeaderField("X-Nintendo-Device-Type", "2");
|
|
|
|
req.addHeaderField("X-Nintendo-Client-ID", "a2efa818a34fa16b8afbc8a74eba3eda");
|
|
req.addHeaderField("X-Nintendo-Client-Secret", "c91cdb5658bd4954ade78533a339cf9a");
|
|
|
|
req.addHeaderField("Accept", "*/*");
|
|
|
|
if(authInfo.region == CafeConsoleRegion::USA)
|
|
req.addHeaderField("X-Nintendo-System-Version", "0270");
|
|
else
|
|
req.addHeaderField("X-Nintendo-System-Version", "0260");
|
|
}
|
|
|
|
void _ACTSetDeviceParameters(CurlRequestHelper& req, AuthInfo& authInfo)
|
|
{
|
|
req.addHeaderField("X-Nintendo-Device-ID", fmt::format("{}", authInfo.deviceId)); // deviceId without platform field
|
|
req.addHeaderField("X-Nintendo-Serial-Number", authInfo.serial);
|
|
}
|
|
|
|
void _ACTSetRegionAndCountryParameters(CurlRequestHelper& req, AuthInfo& authInfo)
|
|
{
|
|
req.addHeaderField("X-Nintendo-Region", fmt::format("{}", (uint32)authInfo.region));
|
|
req.addHeaderField("X-Nintendo-Country", authInfo.country);
|
|
}
|
|
|
|
struct OAuthTokenCacheEntry
|
|
{
|
|
OAuthTokenCacheEntry(std::string_view accountId, std::array<uint8, 32>& passwordHash, std::string_view token, std::string_view refreshToken, uint64 expiresIn) : accountId(accountId), passwordHash(passwordHash), token(token), refreshToken(refreshToken)
|
|
{
|
|
expires = HighResolutionTimer::now().getTickInSeconds() + expiresIn;
|
|
};
|
|
|
|
bool CheckIfSameAccount(const AuthInfo& authInfo) const
|
|
{
|
|
return authInfo.accountId == accountId && authInfo.passwordHash == passwordHash;
|
|
}
|
|
|
|
bool CheckIfExpired() const
|
|
{
|
|
return HighResolutionTimer::now().getTickInSeconds() >= expires;
|
|
}
|
|
std::string accountId;
|
|
std::array<uint8, 32> passwordHash;
|
|
|
|
std::string token;
|
|
std::string refreshToken;
|
|
uint64 expires;
|
|
};
|
|
|
|
std::vector<OAuthTokenCacheEntry> g_oauthTokenCache;
|
|
std::mutex g_oauthTokenCacheMtx;
|
|
|
|
// look up oauth token in cache, otherwise request from server
|
|
ACTOauthToken ACT_GetOauthToken_WithCache(AuthInfo& authInfo, uint64 titleId, uint16 titleVersion)
|
|
{
|
|
ACTOauthToken result{};
|
|
|
|
// check cache first
|
|
g_oauthTokenCacheMtx.lock();
|
|
auto cacheItr = g_oauthTokenCache.begin();
|
|
while (cacheItr != g_oauthTokenCache.end())
|
|
{
|
|
if (cacheItr->CheckIfSameAccount(authInfo))
|
|
{
|
|
if (cacheItr->CheckIfExpired())
|
|
{
|
|
cacheItr = g_oauthTokenCache.erase(cacheItr);
|
|
continue;
|
|
}
|
|
result.token = cacheItr->token;
|
|
result.refreshToken = cacheItr->refreshToken;
|
|
result.apiError = NAPI_RESULT::SUCCESS;
|
|
g_oauthTokenCacheMtx.unlock();
|
|
return result;
|
|
}
|
|
cacheItr++;
|
|
}
|
|
g_oauthTokenCacheMtx.unlock();
|
|
// token not cached, request from server via oauth2
|
|
CurlRequestHelper req;
|
|
|
|
req.initate(fmt::format("{}/v1/api/oauth20/access_token/generate", LaunchSettings::GetActURLPrefix()), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
|
|
_ACTSetCommonHeaderParameters(req, authInfo);
|
|
_ACTSetDeviceParameters(req, authInfo);
|
|
_ACTSetRegionAndCountryParameters(req, authInfo);
|
|
req.addHeaderField("X-Nintendo-Device-Cert", authInfo.deviceCertBase64);
|
|
|
|
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
|
|
req.addHeaderField("X-Nintendo-Environment", "L1");
|
|
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", titleId));
|
|
uint32 uniqueId = ((titleId >> 8) & 0xFFFFF);
|
|
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
|
|
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", titleVersion));
|
|
|
|
// convert password hash to string
|
|
char passwordHashString[128];
|
|
for (sint32 i = 0; i < 32; i++)
|
|
sprintf(passwordHashString + i * 2, "%02x", authInfo.passwordHash[i]);
|
|
req.addPostField("grant_type", "password");
|
|
req.addPostField("user_id", authInfo.accountId);
|
|
req.addPostField("password", passwordHashString);
|
|
req.addPostField("password_type", "hash");
|
|
|
|
req.addHeaderField("Content-type", "application/x-www-form-urlencoded");
|
|
|
|
if (!req.submitRequest(true))
|
|
{
|
|
cemuLog_log(LogType::Force, fmt::format("Failed request /oauth20/access_token/generate"));
|
|
result.apiError = NAPI_RESULT::FAILED;
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
Response example:
|
|
<OAuth20>
|
|
<access_token>
|
|
<token>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</token>
|
|
<refresh_token>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</refresh_token>
|
|
<expires_in>3600</expires_in>
|
|
</access_token>
|
|
</OAuth20>
|
|
*/
|
|
|
|
// parse result
|
|
pugi::xml_document doc;
|
|
if (!_parseActResponse(req, result, doc))
|
|
return result;
|
|
pugi::xml_node node = doc.child("OAuth20");
|
|
if (!node)
|
|
{
|
|
cemuLog_log(LogType::Force, fmt::format("Response does not contain OAuth20 node"));
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
return result;
|
|
}
|
|
node = node.child("access_token");
|
|
if (!node)
|
|
{
|
|
cemuLog_log(LogType::Force, fmt::format("Response does not contain OAuth20/access_token node"));
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
return result;
|
|
}
|
|
|
|
result.token = node.child_value("token");
|
|
result.refreshToken = node.child_value("refresh_token");
|
|
std::string_view expires_in = node.child_value("expires_in");
|
|
result.apiError = NAPI_RESULT::SUCCESS;
|
|
|
|
if (result.token.empty())
|
|
cemuLog_force("OAuth20/token is empty");
|
|
sint64 expiration = StringHelpers::ToInt64(expires_in);
|
|
expiration = std::max(expiration - 30LL, 0LL); // subtract a few seconds to compensate for the web request delay
|
|
|
|
// update cache
|
|
if (expiration > 0)
|
|
{
|
|
g_oauthTokenCacheMtx.lock();
|
|
g_oauthTokenCache.emplace_back(authInfo.accountId, authInfo.passwordHash, result.token, result.refreshToken, expiration);
|
|
g_oauthTokenCacheMtx.unlock();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool ACT_GetProfile(AuthInfo& authInfo, ACTGetProfileResult& result)
|
|
{
|
|
CurlRequestHelper req;
|
|
|
|
req.initate(fmt::format("{}/v1/api/people/@me/profile", LaunchSettings::GetActURLPrefix()), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
|
|
|
|
_ACTSetCommonHeaderParameters(req, authInfo);
|
|
_ACTSetDeviceParameters(req, authInfo);
|
|
|
|
// get oauth2 token
|
|
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, 0x0005001010001C00, 0x0001C);
|
|
|
|
|
|
cemu_assert_unimplemented();
|
|
return true;
|
|
}
|
|
|
|
struct NexTokenCacheEntry
|
|
{
|
|
NexTokenCacheEntry(std::string_view accountId, std::array<uint8, 32>& passwordHash, uint32 gameServerId, ACTNexToken& nexToken) : accountId(accountId), passwordHash(passwordHash), nexToken(nexToken), gameServerId(gameServerId) {};
|
|
|
|
bool IsMatch(const AuthInfo& authInfo, const uint32 gameServerId) const
|
|
{
|
|
return authInfo.accountId == accountId && authInfo.passwordHash == passwordHash && this->gameServerId == gameServerId;
|
|
}
|
|
|
|
std::string accountId;
|
|
std::array<uint8, 32> passwordHash;
|
|
uint32 gameServerId;
|
|
|
|
ACTNexToken nexToken;
|
|
};
|
|
|
|
std::vector<NexTokenCacheEntry> g_nexTokenCache;
|
|
std::mutex g_nexTokenCacheMtx;
|
|
|
|
ACTGetNexTokenResult ACT_GetNexToken_WithCache(AuthInfo& authInfo, uint64 titleId, uint16 titleVersion, uint32 serverId)
|
|
{
|
|
ACTGetNexTokenResult result{};
|
|
// check cache
|
|
g_nexTokenCacheMtx.lock();
|
|
for (auto& itr : g_nexTokenCache)
|
|
{
|
|
if (itr.IsMatch(authInfo, serverId))
|
|
{
|
|
result.nexToken = itr.nexToken;
|
|
result.apiError = NAPI_RESULT::SUCCESS;
|
|
g_nexTokenCacheMtx.unlock();
|
|
return result;
|
|
|
|
}
|
|
}
|
|
g_nexTokenCacheMtx.unlock();
|
|
// get Nex token
|
|
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, titleId, titleVersion);
|
|
if (!oauthToken.isValid())
|
|
{
|
|
cemuLog_force("ACT_GetNexToken(): Failed to retrieve OAuth token");
|
|
if (oauthToken.apiError == NAPI_RESULT::SERVICE_ERROR)
|
|
{
|
|
result.apiError = NAPI_RESULT::SERVICE_ERROR;
|
|
result.serviceError = oauthToken.serviceError;
|
|
}
|
|
else
|
|
{
|
|
result.apiError = NAPI_RESULT::DATA_ERROR;
|
|
}
|
|
return result;
|
|
}
|
|
// do request
|
|
CurlRequestHelper req;
|
|
req.initate(fmt::format("{}/v1/api/provider/nex_token/@me?game_server_id={:08X}", LaunchSettings::GetActURLPrefix(), serverId), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
|
|
_ACTSetCommonHeaderParameters(req, authInfo);
|
|
_ACTSetDeviceParameters(req, authInfo);
|
|
_ACTSetRegionAndCountryParameters(req, authInfo);
|
|
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
|
|
req.addHeaderField("X-Nintendo-Environment", "L1");
|
|
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", titleId));
|
|
uint32 uniqueId = ((titleId >> 8) & 0xFFFFF);
|
|
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
|
|
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", titleVersion));
|
|
|
|
req.addHeaderField("Authorization", fmt::format("Bearer {}", oauthToken.token));
|
|
|
|
if (!req.submitRequest(false))
|
|
{
|
|
cemuLog_log(LogType::Force, fmt::format("Failed request /provider/nex_token/@me"));
|
|
result.apiError = NAPI_RESULT::FAILED;
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
Example response (success):
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<nex_token>
|
|
<host>HOST</host>
|
|
<nex_password>xxxxxxxxxxxxxxxx</nex_password>
|
|
<pid>123456</pid>
|
|
<port>60200</port>
|
|
<token>xxxxxxxxxxxxxxx</token>
|
|
</nex_token>
|
|
|
|
|
|
Example response (error case):
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<errors>
|
|
<error>
|
|
<code>1021</code>
|
|
<message>The requested game server was not found.</message>
|
|
</error>
|
|
</errors>
|
|
*/
|
|
|
|
// code 0124 -> Version lower than useable registered
|
|
|
|
// parse result
|
|
pugi::xml_document doc;
|
|
if (!_parseActResponse(req, result, doc))
|
|
return result;
|
|
pugi::xml_node tokenNode = doc.child("nex_token");
|
|
if (!tokenNode)
|
|
{
|
|
cemuLog_force("Response does not contain NexToken node");
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
return result;
|
|
}
|
|
|
|
std::string_view host = tokenNode.child_value("host");
|
|
std::string_view nex_password = tokenNode.child_value("nex_password");
|
|
std::string_view port = tokenNode.child_value("port");
|
|
std::string_view token = tokenNode.child_value("token");
|
|
|
|
std::memset(&result.nexToken, 0, sizeof(result.nexToken));
|
|
if (host.size() > 15)
|
|
cemuLog_force("NexToken response: host field too long");
|
|
if (nex_password.size() > 64)
|
|
cemuLog_force("NexToken response: nex_password field too long");
|
|
if (token.size() > 512)
|
|
cemuLog_force("NexToken response: token field too long");
|
|
for (size_t i = 0; i < std::min(host.size(), (size_t)15); i++)
|
|
result.nexToken.host[i] = host[i];
|
|
for (size_t i = 0; i < std::min(nex_password.size(), (size_t)64); i++)
|
|
result.nexToken.nexPassword[i] = nex_password[i];
|
|
for (size_t i = 0; i < std::min(token.size(), (size_t)512); i++)
|
|
result.nexToken.token[i] = token[i];
|
|
result.nexToken.port = (uint16)StringHelpers::ToInt(port);
|
|
result.apiError = NAPI_RESULT::SUCCESS;
|
|
g_nexTokenCacheMtx.lock();
|
|
g_nexTokenCache.emplace_back(authInfo.accountId, authInfo.passwordHash, serverId, result.nexToken);
|
|
g_nexTokenCacheMtx.unlock();
|
|
return result;
|
|
}
|
|
|
|
struct IndependentTokenCacheEntry
|
|
{
|
|
IndependentTokenCacheEntry(std::string_view accountId, std::array<uint8, 32>& passwordHash, std::string_view clientId, std::string_view independentToken, sint64 expiresIn) : accountId(accountId), passwordHash(passwordHash), clientId(clientId), independentToken(independentToken)
|
|
{
|
|
expires = HighResolutionTimer::now().getTickInSeconds() + expiresIn;
|
|
};
|
|
|
|
bool IsMatch(const AuthInfo& authInfo, const std::string_view clientId) const
|
|
{
|
|
return authInfo.accountId == accountId && authInfo.passwordHash == passwordHash && this->clientId == clientId;
|
|
}
|
|
|
|
bool CheckIfExpired() const
|
|
{
|
|
return (sint64)HighResolutionTimer::now().getTickInSeconds() >= expires;
|
|
}
|
|
|
|
std::string accountId;
|
|
std::array<uint8, 32> passwordHash;
|
|
std::string clientId;
|
|
sint64 expires;
|
|
|
|
std::string independentToken;
|
|
};
|
|
|
|
std::vector<IndependentTokenCacheEntry> g_IndependentTokenCache;
|
|
std::mutex g_IndependentTokenCacheMtx;
|
|
|
|
ACTGetIndependentTokenResult ACT_GetIndependentToken_WithCache(AuthInfo& authInfo, uint64 titleId, uint16 titleVersion, std::string_view clientId)
|
|
{
|
|
ACTGetIndependentTokenResult result{};
|
|
// check cache
|
|
g_IndependentTokenCacheMtx.lock();
|
|
auto itr = g_IndependentTokenCache.begin();
|
|
while(itr != g_IndependentTokenCache.end())
|
|
{
|
|
if (itr->CheckIfExpired())
|
|
{
|
|
itr = g_IndependentTokenCache.erase(itr);
|
|
continue;
|
|
}
|
|
else if (itr->IsMatch(authInfo, clientId))
|
|
{
|
|
result.token = itr->independentToken;
|
|
result.expiresIn = std::max(itr->expires - (sint64)HighResolutionTimer::now().getTickInSeconds(), (sint64)0);
|
|
result.apiError = NAPI_RESULT::SUCCESS;
|
|
g_IndependentTokenCacheMtx.unlock();
|
|
return result;
|
|
}
|
|
itr++;
|
|
}
|
|
g_IndependentTokenCacheMtx.unlock();
|
|
// get Independent token
|
|
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, titleId, titleVersion);
|
|
if (!oauthToken.isValid())
|
|
{
|
|
cemuLog_force("ACT_GetIndependentToken(): Failed to retrieve OAuth token");
|
|
if (oauthToken.apiError == NAPI_RESULT::SERVICE_ERROR)
|
|
{
|
|
result.apiError = NAPI_RESULT::SERVICE_ERROR;
|
|
result.serviceError = oauthToken.serviceError;
|
|
}
|
|
else
|
|
{
|
|
result.apiError = NAPI_RESULT::DATA_ERROR;
|
|
}
|
|
return result;
|
|
}
|
|
// do request
|
|
CurlRequestHelper req;
|
|
req.initate(fmt::format("{}/v1/api/provider/service_token/@me?client_id={}", LaunchSettings::GetActURLPrefix(), clientId), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
|
|
_ACTSetCommonHeaderParameters(req, authInfo);
|
|
_ACTSetDeviceParameters(req, authInfo);
|
|
_ACTSetRegionAndCountryParameters(req, authInfo);
|
|
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
|
|
req.addHeaderField("X-Nintendo-Environment", "L1");
|
|
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", titleId));
|
|
uint32 uniqueId = ((titleId >> 8) & 0xFFFFF);
|
|
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
|
|
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", titleVersion));
|
|
|
|
req.addHeaderField("Authorization", fmt::format("Bearer {}", oauthToken.token));
|
|
|
|
if (!req.submitRequest(false))
|
|
{
|
|
cemuLog_log(LogType::Force, fmt::format("Failed request /provider/service_token/@me"));
|
|
result.apiError = NAPI_RESULT::FAILED;
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
Example response (success):
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<service_token>
|
|
<token>xxxxxxxxxxxx</token>
|
|
</service_token>
|
|
*/
|
|
|
|
// parse result
|
|
pugi::xml_document doc;
|
|
if (!_parseActResponse(req, result, doc))
|
|
return result;
|
|
pugi::xml_node tokenNode = doc.child("service_token");
|
|
if (!tokenNode)
|
|
{
|
|
cemuLog_force("Response does not contain service_token node");
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
return result;
|
|
}
|
|
|
|
std::string_view token = tokenNode.child_value("token");
|
|
result.token = token;
|
|
result.apiError = NAPI_RESULT::SUCCESS;
|
|
|
|
g_IndependentTokenCacheMtx.lock();
|
|
g_IndependentTokenCache.emplace_back(authInfo.accountId, authInfo.passwordHash, clientId, result.token, 3600);
|
|
g_IndependentTokenCacheMtx.unlock();
|
|
return result;
|
|
}
|
|
|
|
ACTConvertNnidToPrincipalIdResult ACT_ACTConvertNnidToPrincipalId(AuthInfo& authInfo, std::string_view nnid)
|
|
{
|
|
ACTConvertNnidToPrincipalIdResult result{};
|
|
// get Independent token
|
|
ACTOauthToken oauthToken = ACT_GetOauthToken_WithCache(authInfo, 0x0005001010001C00, 0);
|
|
if (!oauthToken.isValid())
|
|
{
|
|
cemuLog_force("ACT_ACTConvertNnidToPrincipalId(): Failed to retrieve OAuth token");
|
|
if (oauthToken.apiError == NAPI_RESULT::SERVICE_ERROR)
|
|
{
|
|
result.apiError = NAPI_RESULT::SERVICE_ERROR;
|
|
result.serviceError = oauthToken.serviceError;
|
|
}
|
|
else
|
|
{
|
|
result.apiError = NAPI_RESULT::DATA_ERROR;
|
|
}
|
|
return result;
|
|
}
|
|
// do request
|
|
CurlRequestHelper req;
|
|
req.initate(fmt::format("{}/v1/api/admin/mapped_ids?input_type=user_id&output_type=pid&input={}", LaunchSettings::GetActURLPrefix(), nnid), CurlRequestHelper::SERVER_SSL_CONTEXT::ACT);
|
|
_ACTSetCommonHeaderParameters(req, authInfo);
|
|
_ACTSetDeviceParameters(req, authInfo);
|
|
_ACTSetRegionAndCountryParameters(req, authInfo);
|
|
req.addHeaderField("X-Nintendo-FPD-Version", "0000");
|
|
req.addHeaderField("X-Nintendo-Environment", "L1");
|
|
req.addHeaderField("X-Nintendo-Title-ID", fmt::format("{:016x}", 0x0005001010001C00));
|
|
uint32 uniqueId = 0x50010;
|
|
req.addHeaderField("X-Nintendo-Unique-ID", fmt::format("{:05x}", uniqueId));
|
|
req.addHeaderField("X-Nintendo-Application-Version", fmt::format("{:04x}", 0));
|
|
|
|
req.addHeaderField("Authorization", fmt::format("Bearer {}", oauthToken.token));
|
|
|
|
if (!req.submitRequest(false))
|
|
{
|
|
cemuLog_log(LogType::Force, fmt::format("Failed request /admin/mapped_ids"));
|
|
result.apiError = NAPI_RESULT::FAILED;
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
Example response (success):
|
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<mapped_ids>
|
|
<mapped_id>
|
|
<in_id>input-nnid</in_id>
|
|
<out_id>12345</out_id>
|
|
</mapped_id>
|
|
</mapped_ids>
|
|
*/
|
|
|
|
// parse result
|
|
pugi::xml_document doc;
|
|
if (!_parseActResponse(req, result, doc))
|
|
return result;
|
|
pugi::xml_node tokenNode = doc.child("mapped_ids");
|
|
if (!tokenNode)
|
|
{
|
|
cemuLog_force("Response does not contain mapped_ids node");
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
return result;
|
|
}
|
|
tokenNode = tokenNode.child("mapped_id");
|
|
if (!tokenNode)
|
|
{
|
|
cemuLog_force("Response does not contain mapped_id node");
|
|
result.apiError = NAPI_RESULT::XML_ERROR;
|
|
return result;
|
|
}
|
|
std::string_view pidString = tokenNode.child_value("out_id");
|
|
if (!pidString.empty())
|
|
{
|
|
result.isFound = true;
|
|
result.principalId = StringHelpers::ToInt(pidString);
|
|
}
|
|
else
|
|
{
|
|
result.isFound = false;
|
|
result.principalId = 0;
|
|
}
|
|
result.apiError = NAPI_RESULT::SUCCESS;
|
|
return result;
|
|
}
|
|
|
|
bool NAPI_MakeAuthInfoFromCurrentAccount(AuthInfo& authInfo)
|
|
{
|
|
authInfo = {};
|
|
if (!NCrypto::SEEPROM_IsPresent())
|
|
return false;
|
|
const Account& account = Account::GetCurrentAccount();
|
|
authInfo.accountId = account.GetAccountId();
|
|
auto passwordHash = account.GetAccountPasswordCache();
|
|
authInfo.passwordHash = passwordHash;
|
|
if (std::all_of(passwordHash.cbegin(), passwordHash.cend(), [](uint8 v) { return v == 0; }))
|
|
{
|
|
static bool s_showedLoginError = false;
|
|
if (!s_showedLoginError)
|
|
{
|
|
cemuLog_force("Account login is impossible because the cached password hash is not set");
|
|
s_showedLoginError = true;
|
|
}
|
|
return false; // password hash not set
|
|
}
|
|
authInfo.deviceId = NCrypto::GetDeviceId();
|
|
authInfo.serial = NCrypto::GetSerial();
|
|
authInfo.region = NCrypto::SEEPROM_GetRegion();
|
|
authInfo.country = NCrypto::GetCountryAsString(account.GetCountry());
|
|
authInfo.deviceCertBase64 = NCrypto::CertECC::GetDeviceCertificate().encodeToBase64();
|
|
|
|
return true;
|
|
}
|
|
}
|