mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-12 09:45:04 -04:00
746 lines
27 KiB
C++
746 lines
27 KiB
C++
#include "network_internal.h"
|
|
|
|
namespace NetworkHle {
|
|
|
|
enum SslError {
|
|
SSL_OK = 0,
|
|
SSL_ERR_FAILED = -1,
|
|
SSL_ERR_RAGAIN = -2,
|
|
SSL_ERR_WAGAIN = -3,
|
|
SSL_ERR_SYSCALL = -5,
|
|
SSL_ERR_ZERO = -6,
|
|
SSL_ERR_ID = -8,
|
|
SSL_ERR_VCOMMONNAME = -9,
|
|
};
|
|
|
|
enum SslIoctlv {
|
|
IOCTLV_NET_SSL_NEW = 0x01,
|
|
IOCTLV_NET_SSL_CONNECT = 0x02,
|
|
IOCTLV_NET_SSL_DOHANDSHAKE = 0x03,
|
|
IOCTLV_NET_SSL_READ = 0x04,
|
|
IOCTLV_NET_SSL_WRITE = 0x05,
|
|
IOCTLV_NET_SSL_SHUTDOWN = 0x06,
|
|
IOCTLV_NET_SSL_SETCLIENTCERT = 0x07,
|
|
IOCTLV_NET_SSL_SETCLIENTCERTDEFAULT = 0x08,
|
|
IOCTLV_NET_SSL_REMOVECLIENTCERT = 0x09,
|
|
IOCTLV_NET_SSL_SETROOTCA = 0x0A,
|
|
IOCTLV_NET_SSL_SETROOTCADEFAULT = 0x0B,
|
|
IOCTLV_NET_SSL_DOHANDSHAKEEX = 0x0C,
|
|
IOCTLV_NET_SSL_SETBUILTINROOTCA = 0x0D,
|
|
IOCTLV_NET_SSL_SETBUILTINCLIENTCERT = 0x0E,
|
|
IOCTLV_NET_SSL_DISABLEVERIFYOPTIONFORDEBUG = 0x0F,
|
|
IOCTLV_NET_SSL_DEBUGGETVERSION = 0x14,
|
|
IOCTLV_NET_SSL_DEBUGGETTIME = 0x15,
|
|
};
|
|
|
|
constexpr int kMaxSslSessions = 4;
|
|
|
|
struct SslSession {
|
|
bool active = false;
|
|
bool handshaked = false;
|
|
bool plaintextWfc = false;
|
|
uint32_t socketFd = UINT32_MAX;
|
|
NativeSocket native = kInvalidSocket;
|
|
std::string hostname;
|
|
std::vector<uint8_t> nasWriteBuffer;
|
|
std::vector<uint8_t> decrypted;
|
|
std::vector<uint8_t> encryptedExtra;
|
|
// Failure-report one-shots; cleared with the session by ClearSslSession. The
|
|
// handshake re-runs on every read/write, so a failing one repeats forever.
|
|
bool loggedHandshakeFail = false;
|
|
int32_t lastLoggedReadError = 0;
|
|
int32_t lastLoggedWriteError = 0;
|
|
#ifdef _WIN32
|
|
bool haveCred = false;
|
|
bool haveContext = false;
|
|
CredHandle cred{};
|
|
CtxtHandle context{};
|
|
SecPkgContext_StreamSizes sizes{};
|
|
#endif
|
|
};
|
|
|
|
static std::array<SslSession, kMaxSslSessions> g_sslSessions;
|
|
|
|
// Logging wrapper around the platform handshake implementation; see below.
|
|
static int32_t SslHandshake(SslSession& ssl);
|
|
|
|
static bool IsRetroNasSslHost(std::string_view hostname) {
|
|
if (!RetroRewindProfileActive()) {
|
|
return false;
|
|
}
|
|
const std::string lowered = Lower(hostname);
|
|
return StartsWith(lowered, "nas.") || StartsWith(lowered, "naswii.");
|
|
}
|
|
|
|
static bool IsRetroPlaintextSslHost(std::string_view hostname) {
|
|
if (IsRetroNasSslHost(hostname)) {
|
|
return true;
|
|
}
|
|
if (!RetroRewindProfileActive()) {
|
|
return false;
|
|
}
|
|
|
|
const std::string lowered = Lower(hostname);
|
|
return StartsWith(lowered, "sake.gs.") ||
|
|
lowered.find(".sake.gs.") != std::string::npos ||
|
|
StartsWith(lowered, "gamestats.gs.") ||
|
|
lowered.find(".gamestats.gs.") != std::string::npos ||
|
|
StartsWith(lowered, "gamestats2.gs.") ||
|
|
lowered.find(".gamestats2.gs.") != std::string::npos ||
|
|
StartsWith(lowered, "race.gs.") ||
|
|
lowered.find(".race.gs.") != std::string::npos;
|
|
}
|
|
|
|
static std::optional<size_t> ParseHttpContentLength(std::string_view headers) {
|
|
std::optional<size_t> parsedLength;
|
|
size_t lineStart = 0;
|
|
while (lineStart <= headers.size()) {
|
|
const size_t lineEnd = headers.find("\r\n", lineStart);
|
|
const std::string_view line = headers.substr(
|
|
lineStart, lineEnd == std::string_view::npos ? headers.size() - lineStart : lineEnd - lineStart);
|
|
const size_t colon = line.find(':');
|
|
if (colon != std::string_view::npos && Lower(line.substr(0, colon)) == "content-length") {
|
|
size_t valueStart = colon + 1;
|
|
while (valueStart < line.size() && (line[valueStart] == ' ' || line[valueStart] == '\t')) {
|
|
++valueStart;
|
|
}
|
|
size_t valueEnd = line.size();
|
|
while (valueEnd > valueStart && (line[valueEnd - 1] == ' ' || line[valueEnd - 1] == '\t')) {
|
|
--valueEnd;
|
|
}
|
|
size_t value = 0;
|
|
const auto [end, error] = std::from_chars(
|
|
line.data() + valueStart, line.data() + valueEnd, value, 10);
|
|
if (error != std::errc{} || end != line.data() + valueEnd ||
|
|
(parsedLength && *parsedLength != value)) {
|
|
return std::nullopt;
|
|
}
|
|
parsedLength = value;
|
|
}
|
|
if (lineEnd == std::string_view::npos) {
|
|
break;
|
|
}
|
|
lineStart = lineEnd + 2;
|
|
}
|
|
return parsedLength;
|
|
}
|
|
|
|
// The Retro-WFC server rejects a NAS "POST /ac" auth body split across TCP
|
|
// segments, so buffer guest chunks until Content-Length is satisfied, then
|
|
// flush as one write. Flush at 16 KiB if it never terminates, and immediately
|
|
// if Content-Length can't be parsed.
|
|
static NasSslWriteAction AccumulateNasRequest(std::vector<uint8_t>& buffer, const uint8_t* data,
|
|
uint32_t size, std::vector<uint8_t>& patched) {
|
|
buffer.insert(buffer.end(), data, data + size);
|
|
if (buffer.size() > 16 * 1024) {
|
|
patched.swap(buffer);
|
|
buffer.clear();
|
|
return NasSslWriteAction::Ready;
|
|
}
|
|
|
|
const std::string accumulated(reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
|
const size_t headerEnd = accumulated.find("\r\n\r\n");
|
|
if (headerEnd == std::string::npos) {
|
|
return NasSslWriteAction::Buffered;
|
|
}
|
|
|
|
const std::optional<size_t> contentLength =
|
|
ParseHttpContentLength(std::string_view(accumulated).substr(0, headerEnd));
|
|
if (!contentLength) {
|
|
patched.swap(buffer);
|
|
buffer.clear();
|
|
return NasSslWriteAction::Ready;
|
|
}
|
|
|
|
const size_t requestSize = headerEnd + 4 + *contentLength;
|
|
if (accumulated.size() < requestSize) {
|
|
return NasSslWriteAction::Buffered;
|
|
}
|
|
|
|
patched.assign(buffer.begin(), buffer.begin() + requestSize);
|
|
if (accumulated.size() > requestSize) {
|
|
patched.insert(patched.end(), buffer.begin() + requestSize, buffer.end());
|
|
}
|
|
buffer.clear();
|
|
return NasSslWriteAction::Ready;
|
|
}
|
|
|
|
static bool StartsNasAuthRequest(const uint8_t* data, uint32_t size) {
|
|
return size >= 9 && std::memcmp(data, "POST /ac ", 9) == 0;
|
|
}
|
|
|
|
// SSL route: the session carries the hostname the guest asked for, so the NAS
|
|
// host is identified by name. Deliberately not IsRetroNasSslHost - the SSL write
|
|
// path re-assembles NAS auth regardless of which profile is active.
|
|
static NasSslWriteAction PrepareNasSslWrite(SslSession& ssl, const uint8_t* data, uint32_t size,
|
|
std::vector<uint8_t>& patched) {
|
|
if (!data || size == 0) {
|
|
return NasSslWriteAction::PassThrough;
|
|
}
|
|
const std::string loweredHost = Lower(ssl.hostname);
|
|
const bool isNasHost = StartsWith(loweredHost, "nas.") || StartsWith(loweredHost, "naswii.");
|
|
if (!isNasHost || (!StartsNasAuthRequest(data, size) && ssl.nasWriteBuffer.empty())) {
|
|
return NasSslWriteAction::PassThrough;
|
|
}
|
|
return AccumulateNasRequest(ssl.nasWriteBuffer, data, size, patched);
|
|
}
|
|
|
|
// Plain-TCP route: a rerouted 443->80 NAS connection has no hostname on the
|
|
// socket, so the peer port and the stream type are what identify it.
|
|
NasSslWriteAction PreparePlainNasTcpWrite(WiiSocket& socket, const uint8_t* data, uint32_t size,
|
|
std::vector<uint8_t>& patched) {
|
|
if (!data || size == 0 || socket.type != SOCK_STREAM || socket.peerPort != 80) {
|
|
return NasSslWriteAction::PassThrough;
|
|
}
|
|
if (!StartsNasAuthRequest(data, size) && socket.nasWriteBuffer.empty()) {
|
|
return NasSslWriteAction::PassThrough;
|
|
}
|
|
return AccumulateNasRequest(socket.nasWriteBuffer, data, size, patched);
|
|
}
|
|
|
|
static bool WriteSslReturn(const std::vector<IoVector>& in, int32_t value) {
|
|
if (in.empty() || !in[0].address || in[0].size < 4) {
|
|
return false;
|
|
}
|
|
Memory::Write32(in[0].address, static_cast<uint32_t>(value));
|
|
return true;
|
|
}
|
|
|
|
static int ReadSslId(const std::vector<IoVector>& out) {
|
|
if (out.empty() || !out[0].address || out[0].size < 4) {
|
|
return -1;
|
|
}
|
|
return static_cast<int>(Memory::Read32(out[0].address)) - 1;
|
|
}
|
|
|
|
static bool IsSslIdValid(int id) {
|
|
return id >= 0 && id < kMaxSslSessions && g_sslSessions[id].active;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static bool IsSecuritySuccess(SECURITY_STATUS status) {
|
|
return status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED || status == SEC_I_INCOMPLETE_CREDENTIALS;
|
|
}
|
|
|
|
static bool SendAll(NativeSocket socket, const uint8_t* data, size_t size) {
|
|
size_t offset = 0;
|
|
while (offset < size) {
|
|
const int chunk = static_cast<int>(std::min<size_t>(size - offset, 64 * 1024));
|
|
const int ret = send(socket, reinterpret_cast<const char*>(data + offset), chunk, 0);
|
|
if (ret <= 0) {
|
|
// Reported by the SSL_WRITE / handshake caller as SSL_ERR_SYSCALL.
|
|
return false;
|
|
}
|
|
offset += static_cast<size_t>(ret);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int RecvBlocking(NativeSocket socket, std::vector<uint8_t>& buffer) {
|
|
std::array<uint8_t, 16 * 1024> temp{};
|
|
const int ret = recv(socket, reinterpret_cast<char*>(temp.data()), static_cast<int>(temp.size()), 0);
|
|
if (ret > 0) {
|
|
buffer.insert(buffer.end(), temp.begin(), temp.begin() + ret);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void KeepExtraBuffer(std::vector<uint8_t>& dest, const SecBuffer& buffer) {
|
|
dest.clear();
|
|
if (buffer.BufferType == SECBUFFER_EXTRA && buffer.pvBuffer && buffer.cbBuffer) {
|
|
const auto* begin = static_cast<const uint8_t*>(buffer.pvBuffer);
|
|
dest.assign(begin, begin + buffer.cbBuffer);
|
|
}
|
|
}
|
|
|
|
static void ClearSslSession(SslSession& ssl) {
|
|
if (ssl.haveContext) {
|
|
DeleteSecurityContext(&ssl.context);
|
|
}
|
|
if (ssl.haveCred) {
|
|
FreeCredentialsHandle(&ssl.cred);
|
|
}
|
|
ssl = {};
|
|
}
|
|
|
|
static int32_t EnsureSslCredentials(SslSession& ssl) {
|
|
if (ssl.haveCred) {
|
|
return SSL_OK;
|
|
}
|
|
|
|
SCHANNEL_CRED cred{};
|
|
cred.dwVersion = SCHANNEL_CRED_VERSION;
|
|
// Leave certificate validation to Schannel. SCH_CRED_MANUAL_CRED_VALIDATION
|
|
// suppresses that validation and requires an explicit CertGetCertificateChain
|
|
// implementation, which this HLE does not provide. The target hostname passed
|
|
// to InitializeSecurityContextA below is therefore checked together with the
|
|
// server certificate chain.
|
|
cred.dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS |
|
|
SCH_CRED_AUTO_CRED_VALIDATION;
|
|
|
|
TimeStamp expiry{};
|
|
const SECURITY_STATUS status = AcquireCredentialsHandleA(
|
|
nullptr, const_cast<LPSTR>(UNISP_NAME_A), SECPKG_CRED_OUTBOUND, nullptr, &cred, nullptr, nullptr,
|
|
&ssl.cred, &expiry);
|
|
if (status != SEC_E_OK) {
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
ssl.haveCred = true;
|
|
return SSL_OK;
|
|
}
|
|
|
|
static int32_t SslHandshakeImpl(SslSession& ssl) {
|
|
if (ssl.plaintextWfc) {
|
|
ssl.handshaked = true;
|
|
return SSL_OK;
|
|
}
|
|
|
|
// Schannel can authenticate a certificate chain without authenticating a
|
|
// server identity when no target name is supplied. Refuse that ambiguous
|
|
// mode rather than accepting a certificate for an unrelated endpoint.
|
|
if (ssl.hostname.empty()) {
|
|
return SSL_ERR_VCOMMONNAME;
|
|
}
|
|
|
|
const int32_t credRet = EnsureSslCredentials(ssl);
|
|
if (credRet != SSL_OK) {
|
|
return credRet;
|
|
}
|
|
if (ssl.native == kInvalidSocket) {
|
|
return SSL_ERR_SYSCALL;
|
|
}
|
|
if (ssl.handshaked) {
|
|
return SSL_OK;
|
|
}
|
|
|
|
DWORD attrs = 0;
|
|
TimeStamp expiry{};
|
|
std::vector<uint8_t> incoming = std::move(ssl.encryptedExtra);
|
|
ssl.encryptedExtra.clear();
|
|
constexpr DWORD flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY |
|
|
ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM | ISC_REQ_EXTENDED_ERROR;
|
|
|
|
for (int step = 0; step < 128; ++step) {
|
|
SecBuffer outBuffer{};
|
|
outBuffer.BufferType = SECBUFFER_TOKEN;
|
|
SecBufferDesc outDesc{};
|
|
outDesc.ulVersion = SECBUFFER_VERSION;
|
|
outDesc.cBuffers = 1;
|
|
outDesc.pBuffers = &outBuffer;
|
|
|
|
SecBuffer inBuffers[2]{};
|
|
SecBufferDesc inDesc{};
|
|
SecBufferDesc* inDescPtr = nullptr;
|
|
if (!incoming.empty()) {
|
|
inBuffers[0].BufferType = SECBUFFER_TOKEN;
|
|
inBuffers[0].pvBuffer = incoming.data();
|
|
inBuffers[0].cbBuffer = static_cast<unsigned long>(incoming.size());
|
|
inBuffers[1].BufferType = SECBUFFER_EMPTY;
|
|
inDesc.ulVersion = SECBUFFER_VERSION;
|
|
inDesc.cBuffers = 2;
|
|
inDesc.pBuffers = inBuffers;
|
|
inDescPtr = &inDesc;
|
|
}
|
|
|
|
const SECURITY_STATUS status = InitializeSecurityContextA(
|
|
&ssl.cred, ssl.haveContext ? &ssl.context : nullptr,
|
|
const_cast<char*>(ssl.hostname.c_str()), flags, 0, SECURITY_NATIVE_DREP,
|
|
inDescPtr, 0, &ssl.context, &outDesc, &attrs, &expiry);
|
|
if (status != SEC_E_INVALID_HANDLE) {
|
|
ssl.haveContext = true;
|
|
}
|
|
|
|
if (outBuffer.pvBuffer && outBuffer.cbBuffer) {
|
|
const bool sent = SendAll(ssl.native, static_cast<const uint8_t*>(outBuffer.pvBuffer), outBuffer.cbBuffer);
|
|
FreeContextBuffer(outBuffer.pvBuffer);
|
|
if (!sent) {
|
|
return SSL_ERR_SYSCALL;
|
|
}
|
|
}
|
|
|
|
if (status == SEC_E_OK) {
|
|
if (inDescPtr) {
|
|
KeepExtraBuffer(ssl.encryptedExtra, inBuffers[1]);
|
|
}
|
|
const SECURITY_STATUS sizeStatus =
|
|
QueryContextAttributesA(&ssl.context, SECPKG_ATTR_STREAM_SIZES, &ssl.sizes);
|
|
if (sizeStatus != SEC_E_OK) {
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
ssl.handshaked = true;
|
|
return SSL_OK;
|
|
}
|
|
|
|
if (status == SEC_E_INCOMPLETE_MESSAGE) {
|
|
const int ret = RecvBlocking(ssl.native, incoming);
|
|
if (ret == 0) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
if (ret < 0) {
|
|
return SSL_ERR_RAGAIN;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (status == SEC_I_CONTINUE_NEEDED || status == SEC_I_INCOMPLETE_CREDENTIALS) {
|
|
std::vector<uint8_t> extra;
|
|
if (inDescPtr) {
|
|
KeepExtraBuffer(extra, inBuffers[1]);
|
|
}
|
|
incoming = std::move(extra);
|
|
const int ret = RecvBlocking(ssl.native, incoming);
|
|
if (ret == 0) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
if (ret < 0) {
|
|
return SSL_ERR_RAGAIN;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
return status == SEC_E_WRONG_PRINCIPAL ? SSL_ERR_VCOMMONNAME : SSL_ERR_FAILED;
|
|
}
|
|
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
|
|
static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
|
|
if (!data || size == 0) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
const int32_t handshakeRet = SslHandshake(ssl);
|
|
if (handshakeRet != SSL_OK) {
|
|
return handshakeRet;
|
|
}
|
|
|
|
if (ssl.plaintextWfc) {
|
|
return SendAll(ssl.native, data, size) ? static_cast<int32_t>(size) : SSL_ERR_SYSCALL;
|
|
}
|
|
|
|
uint32_t total = 0;
|
|
while (total < size) {
|
|
const uint32_t chunk = std::min<uint32_t>(size - total, ssl.sizes.cbMaximumMessage);
|
|
std::vector<uint8_t> packet(ssl.sizes.cbHeader + chunk + ssl.sizes.cbTrailer);
|
|
std::memcpy(packet.data() + ssl.sizes.cbHeader, data + total, chunk);
|
|
|
|
SecBuffer buffers[4]{};
|
|
buffers[0].BufferType = SECBUFFER_STREAM_HEADER;
|
|
buffers[0].pvBuffer = packet.data();
|
|
buffers[0].cbBuffer = ssl.sizes.cbHeader;
|
|
buffers[1].BufferType = SECBUFFER_DATA;
|
|
buffers[1].pvBuffer = packet.data() + ssl.sizes.cbHeader;
|
|
buffers[1].cbBuffer = chunk;
|
|
buffers[2].BufferType = SECBUFFER_STREAM_TRAILER;
|
|
buffers[2].pvBuffer = packet.data() + ssl.sizes.cbHeader + chunk;
|
|
buffers[2].cbBuffer = ssl.sizes.cbTrailer;
|
|
buffers[3].BufferType = SECBUFFER_EMPTY;
|
|
|
|
SecBufferDesc desc{};
|
|
desc.ulVersion = SECBUFFER_VERSION;
|
|
desc.cBuffers = 4;
|
|
desc.pBuffers = buffers;
|
|
|
|
const SECURITY_STATUS status = EncryptMessage(&ssl.context, 0, &desc, 0);
|
|
if (status != SEC_E_OK) {
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
|
|
const size_t encryptedSize =
|
|
static_cast<size_t>(buffers[0].cbBuffer) + buffers[1].cbBuffer + buffers[2].cbBuffer;
|
|
if (!SendAll(ssl.native, packet.data(), encryptedSize)) {
|
|
return SSL_ERR_SYSCALL;
|
|
}
|
|
total += chunk;
|
|
}
|
|
return static_cast<int32_t>(total);
|
|
}
|
|
|
|
static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
|
|
if (!out || size == 0) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
const int32_t handshakeRet = SslHandshake(ssl);
|
|
if (handshakeRet != SSL_OK) {
|
|
return handshakeRet;
|
|
}
|
|
|
|
if (ssl.plaintextWfc) {
|
|
const int ret = recv(ssl.native, reinterpret_cast<char*>(out), static_cast<int>(size), 0);
|
|
if (ret == 0) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
if (ret < 0) {
|
|
return SSL_ERR_RAGAIN;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
while (ssl.decrypted.empty()) {
|
|
std::vector<uint8_t> encrypted = std::move(ssl.encryptedExtra);
|
|
ssl.encryptedExtra.clear();
|
|
if (encrypted.empty()) {
|
|
const int ret = RecvBlocking(ssl.native, encrypted);
|
|
if (ret == 0) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
if (ret < 0) {
|
|
return SSL_ERR_RAGAIN;
|
|
}
|
|
}
|
|
|
|
for (;;) {
|
|
SecBuffer buffers[4]{};
|
|
buffers[0].BufferType = SECBUFFER_DATA;
|
|
buffers[0].pvBuffer = encrypted.data();
|
|
buffers[0].cbBuffer = static_cast<unsigned long>(encrypted.size());
|
|
buffers[1].BufferType = SECBUFFER_EMPTY;
|
|
buffers[2].BufferType = SECBUFFER_EMPTY;
|
|
buffers[3].BufferType = SECBUFFER_EMPTY;
|
|
|
|
SecBufferDesc desc{};
|
|
desc.ulVersion = SECBUFFER_VERSION;
|
|
desc.cBuffers = 4;
|
|
desc.pBuffers = buffers;
|
|
|
|
const SECURITY_STATUS status = DecryptMessage(&ssl.context, &desc, 0, nullptr);
|
|
if (status == SEC_E_INCOMPLETE_MESSAGE) {
|
|
const int ret = RecvBlocking(ssl.native, encrypted);
|
|
if (ret == 0) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
if (ret < 0) {
|
|
return SSL_ERR_RAGAIN;
|
|
}
|
|
continue;
|
|
}
|
|
if (status == SEC_I_CONTEXT_EXPIRED) {
|
|
return SSL_ERR_ZERO;
|
|
}
|
|
if (!IsSecuritySuccess(status) && status != SEC_I_RENEGOTIATE) {
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
|
|
for (const SecBuffer& buffer : buffers) {
|
|
if (buffer.BufferType == SECBUFFER_DATA && buffer.pvBuffer && buffer.cbBuffer) {
|
|
const auto* begin = static_cast<const uint8_t*>(buffer.pvBuffer);
|
|
ssl.decrypted.insert(ssl.decrypted.end(), begin, begin + buffer.cbBuffer);
|
|
} else if (buffer.BufferType == SECBUFFER_EXTRA && buffer.pvBuffer && buffer.cbBuffer) {
|
|
const auto* begin = static_cast<const uint8_t*>(buffer.pvBuffer);
|
|
ssl.encryptedExtra.assign(begin, begin + buffer.cbBuffer);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
const uint32_t copied = std::min<uint32_t>(size, static_cast<uint32_t>(ssl.decrypted.size()));
|
|
std::memcpy(out, ssl.decrypted.data(), copied);
|
|
ssl.decrypted.erase(ssl.decrypted.begin(), ssl.decrypted.begin() + copied);
|
|
return copied == 0 ? SSL_ERR_ZERO : static_cast<int32_t>(copied);
|
|
}
|
|
#else
|
|
static void ClearSslSession(SslSession& ssl) {
|
|
ssl = {};
|
|
}
|
|
|
|
static int32_t SslHandshakeImpl(SslSession&) {
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
|
|
static int32_t SslWrite(SslSession&, const uint8_t*, uint32_t) {
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
|
|
static int32_t SslRead(SslSession&, uint8_t*, uint32_t) {
|
|
return SSL_ERR_FAILED;
|
|
}
|
|
#endif
|
|
|
|
// The handshake runs on every SSL read/write, so a failure repeats for as long
|
|
// as the session lives; report only the first one.
|
|
static int32_t SslHandshake(SslSession& ssl) {
|
|
const int32_t result = SslHandshakeImpl(ssl);
|
|
if (result != SSL_OK && !ssl.loggedHandshakeFail) {
|
|
ssl.loggedHandshakeFail = true;
|
|
NetFail("ssl handshake FAILED host=%s ssl_err=%d",
|
|
ssl.hostname.empty() ? "?" : ssl.hostname.c_str(), result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void ClearSslSessionsForSocket(uint32_t fd) {
|
|
for (SslSession& ssl : g_sslSessions) {
|
|
if (ssl.active && ssl.socketFd == fd) {
|
|
ClearSslSession(ssl);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std::vector<IoVector>& out) {
|
|
|
|
switch (cmd) {
|
|
case IOCTLV_NET_SSL_NEW: {
|
|
// out[0] carries the guest verify option. Host TLS verification is
|
|
// always enforced by Schannel, but still read this value so a request
|
|
// pointing outside guest memory faults here as it always has.
|
|
if (!out.empty() && out[0].address && out[0].size >= 4) {
|
|
(void)Memory::Read32(out[0].address);
|
|
}
|
|
std::string hostname = out.size() > 1 ? ReadGuestString(out[1].address, out[1].size) : "";
|
|
|
|
for (int i = 0; i < kMaxSslSessions; ++i) {
|
|
if (!g_sslSessions[i].active) {
|
|
ClearSslSession(g_sslSessions[i]);
|
|
g_sslSessions[i].active = true;
|
|
g_sslSessions[i].hostname = hostname;
|
|
WriteSslReturn(in, i + 1);
|
|
return 0;
|
|
}
|
|
}
|
|
WriteSslReturn(in, SSL_ERR_FAILED);
|
|
NetFail("SSL_NEW host=%s FAILED: all %d session slots in use",
|
|
hostname.empty() ? "?" : hostname.c_str(), kMaxSslSessions);
|
|
return 0;
|
|
}
|
|
case IOCTLV_NET_SSL_CONNECT: {
|
|
const int sslId = ReadSslId(out);
|
|
if (!IsSslIdValid(sslId)) {
|
|
WriteSslReturn(in, SSL_ERR_ID);
|
|
return 0;
|
|
}
|
|
if (out.size() < 2 || !out[1].address || out[1].size < 4) {
|
|
WriteSslReturn(in, SSL_ERR_FAILED);
|
|
return 0;
|
|
}
|
|
const uint32_t socketFd = Memory::Read32(out[1].address);
|
|
WiiSocket* socket = GetWiiSocket(socketFd);
|
|
if (!socket) {
|
|
WriteSslReturn(in, SSL_ERR_SYSCALL);
|
|
return 0;
|
|
}
|
|
|
|
SslSession& ssl = g_sslSessions[sslId];
|
|
ssl.socketFd = socketFd;
|
|
ssl.native = socket->native;
|
|
ssl.plaintextWfc = false;
|
|
socket->nonblocking = false;
|
|
SetNonBlocking(socket->native, false);
|
|
if (IsRetroPlaintextSslHost(ssl.hostname) && socket->peerPort == 443) {
|
|
const int32_t reroute = ReconnectWiiSocket(*socket, 80);
|
|
if (reroute != 0) {
|
|
WriteSslReturn(in, SSL_ERR_SYSCALL);
|
|
NetFail("SSL_CONNECT host=%s 443->80 plaintext reroute FAILED wii=%d",
|
|
ssl.hostname.c_str(), reroute);
|
|
return 0;
|
|
}
|
|
ssl.native = socket->native;
|
|
ssl.plaintextWfc = true;
|
|
}
|
|
#ifdef _WIN32
|
|
const int timeoutMs = 15000;
|
|
setsockopt(socket->native, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
|
|
setsockopt(socket->native, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
|
|
#endif
|
|
WriteSslReturn(in, SSL_OK);
|
|
return 0;
|
|
}
|
|
case IOCTLV_NET_SSL_DOHANDSHAKE:
|
|
case IOCTLV_NET_SSL_DOHANDSHAKEEX: {
|
|
const int sslId = ReadSslId(out);
|
|
WriteSslReturn(in, IsSslIdValid(sslId) ? SslHandshake(g_sslSessions[sslId]) : SSL_ERR_ID);
|
|
return 0;
|
|
}
|
|
case IOCTLV_NET_SSL_WRITE: {
|
|
const int sslId = ReadSslId(out);
|
|
if (!IsSslIdValid(sslId)) {
|
|
WriteSslReturn(in, SSL_ERR_ID);
|
|
return 0;
|
|
}
|
|
if (out.size() < 2 || !out[1].address) {
|
|
WriteSslReturn(in, SSL_ERR_FAILED);
|
|
return 0;
|
|
}
|
|
const auto* data = Memory::GetPointer(out[1].address, out[1].size);
|
|
std::vector<uint8_t> patched;
|
|
const NasSslWriteAction nasAction = PrepareNasSslWrite(g_sslSessions[sslId], data, out[1].size, patched);
|
|
if (nasAction == NasSslWriteAction::Buffered) {
|
|
WriteSslReturn(in, static_cast<int32_t>(out[1].size));
|
|
return 0;
|
|
}
|
|
|
|
const bool patchedSslWrite = nasAction == NasSslWriteAction::Ready;
|
|
|
|
const uint8_t* writeData = patchedSslWrite ? patched.data() : data;
|
|
const uint32_t writeSize = patchedSslWrite ? static_cast<uint32_t>(patched.size()) : out[1].size;
|
|
SslSession& writeSession = g_sslSessions[sslId];
|
|
int32_t result = SslWrite(writeSession, writeData, writeSize);
|
|
if (patchedSslWrite && result == static_cast<int32_t>(writeSize)) {
|
|
result = static_cast<int32_t>(out[1].size);
|
|
}
|
|
// SSL_ERR_WAGAIN is a retry, not a failure; the SDK re-issues the write,
|
|
// so only a change of error is reported.
|
|
if (result < 0 && result != SSL_ERR_WAGAIN && result != writeSession.lastLoggedWriteError) {
|
|
writeSession.lastLoggedWriteError = result;
|
|
NetFail("SSL_WRITE host=%s size=%u FAILED ssl_err=%d",
|
|
writeSession.hostname.empty() ? "?" : writeSession.hostname.c_str(), writeSize,
|
|
result);
|
|
}
|
|
WriteSslReturn(in, result);
|
|
return 0;
|
|
}
|
|
case IOCTLV_NET_SSL_READ: {
|
|
const int sslId = ReadSslId(out);
|
|
if (!IsSslIdValid(sslId)) {
|
|
WriteSslReturn(in, SSL_ERR_ID);
|
|
return 0;
|
|
}
|
|
if (in.size() < 2 || !in[1].address) {
|
|
WriteSslReturn(in, SSL_ERR_FAILED);
|
|
return 0;
|
|
}
|
|
auto* data = Memory::GetPointer(in[1].address, in[1].size);
|
|
SslSession& readSession = g_sslSessions[sslId];
|
|
const int32_t result = SslRead(readSession, data, in[1].size);
|
|
if (result < 0 && result != SSL_ERR_RAGAIN && result != readSession.lastLoggedReadError) {
|
|
readSession.lastLoggedReadError = result;
|
|
NetFail("SSL_READ host=%s FAILED ssl_err=%d%s",
|
|
readSession.hostname.empty() ? "?" : readSession.hostname.c_str(), result,
|
|
result == SSL_ERR_ZERO ? " (peer closed)" : "");
|
|
}
|
|
WriteSslReturn(in, result);
|
|
return 0;
|
|
}
|
|
case IOCTLV_NET_SSL_SHUTDOWN: {
|
|
const int sslId = ReadSslId(out);
|
|
if (!IsSslIdValid(sslId)) {
|
|
WriteSslReturn(in, SSL_ERR_ID);
|
|
return 0;
|
|
}
|
|
ClearSslSession(g_sslSessions[sslId]);
|
|
WriteSslReturn(in, SSL_OK);
|
|
return 0;
|
|
}
|
|
case IOCTLV_NET_SSL_SETCLIENTCERT:
|
|
case IOCTLV_NET_SSL_SETCLIENTCERTDEFAULT:
|
|
case IOCTLV_NET_SSL_REMOVECLIENTCERT:
|
|
case IOCTLV_NET_SSL_SETROOTCA:
|
|
case IOCTLV_NET_SSL_SETROOTCADEFAULT:
|
|
case IOCTLV_NET_SSL_SETBUILTINROOTCA:
|
|
case IOCTLV_NET_SSL_SETBUILTINCLIENTCERT:
|
|
case IOCTLV_NET_SSL_DISABLEVERIFYOPTIONFORDEBUG: {
|
|
const int sslId = ReadSslId(out);
|
|
WriteSslReturn(in, IsSslIdValid(sslId) ? SSL_OK : SSL_ERR_ID);
|
|
return 0;
|
|
}
|
|
case IOCTLV_NET_SSL_DEBUGGETVERSION:
|
|
case IOCTLV_NET_SSL_DEBUGGETTIME:
|
|
WriteSslReturn(in, SSL_OK);
|
|
return 0;
|
|
default:
|
|
WriteSslReturn(in, SSL_ERR_FAILED);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
} // namespace NetworkHle
|