Files
ppsspp/Common/Net/Resolve.cpp
T
Henrik Rydgård 618bdc2d72 naett: Let naettInit be called more than once
It asserted that it was the first call, so net::Init - which is documented as
safe to call repeatedly, and is - needed a bool of its own purely to guard this
one line. That's bookkeeping the library may as well do itself, and the WSA half
of the same function already says as much: it doesn't track anything because
WSAStartup counts its own references.

So a repeat call does nothing, and net::Init loses g_naettInitialized. Asking
HTTPSAvailable() twice costs nothing - on Linux it's a cached dlopen result and
everywhere else it's a constant.
2026-09-05 13:31:16 -06:00

737 lines
19 KiB
C++

#include "ppsspp_config.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Net/SocketCompat.h"
#include "Common/Net/Resolve.h"
#ifndef HTTPS_NOT_AVAILABLE
#include "ext/naett-lib/naett.h"
// Note: PPSSPP_PLATFORM(LINUX) is also set on Android, which needs no loader.
#if PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)
// On Linux, naett goes through libcurl, which we load at runtime - so HTTPS support is
// only known once we've tried.
#include "ext/naett-lib/src/naett_curl.h"
#endif
#endif
#if PPSSPP_PLATFORM(ANDROID)
#include <jni.h>
extern JavaVM *gJvm;
#endif
namespace net {
static bool g_wsaInitialized;
void Init() {
#ifdef _WIN32
// WSA does its own internal reference counting, no need to keep track of if we inited or not.
WSADATA wsaData{};
if (FAILED(WSAStartup(MAKEWORD(2, 2), &wsaData))) {
ERROR_LOG(Log::Net, "WSAStartup failed");
} else {
g_wsaInitialized = true;
}
#endif
// naett ignores repeat calls the same way WSAStartup does, so there's nothing to track here
// either. HTTPSAvailable is cheap to ask twice - on Linux it's a cached dlopen result.
#ifndef HTTPS_NOT_AVAILABLE
#if PPSSPP_PLATFORM(ANDROID)
_assert_(gJvm != nullptr);
naettInit(gJvm);
#else
if (HTTPSAvailable()) {
naettInit(NULL);
}
#endif
#endif
}
bool HTTPSAvailable() {
#ifdef HTTPS_NOT_AVAILABLE
return false;
#elif PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)
return naettCurlLoad() != 0;
#else
return true;
#endif
}
void Shutdown() {
#ifdef _WIN32
if (g_wsaInitialized) {
WSACleanup();
}
#endif
}
bool HostPortExists(const std::string &host, int port, int timeout_ms) {
if (host.empty() || (port <= 0 || port > 65535) || timeout_ms < 0) return false;
addrinfo hints;
addrinfo* res = nullptr;
std::memset(&hints, 0, sizeof(hints));
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
int gai = getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res);
if (gai != 0) {
// getaddrinfo failed (DNS resolve failed or bad port)
return false;
}
bool ok = false;
for (addrinfo* p = res; p != nullptr && !ok; p = p->ai_next) {
// create socket
int sockfd =
#ifdef _WIN32
(int)socket(p->ai_family, p->ai_socktype, p->ai_protocol);
#else
socket(p->ai_family, p->ai_socktype, p->ai_protocol);
#endif
if (sockfd < 0) {
continue;
}
// make non-blocking
#ifdef _WIN32
unsigned long mode = 1;
ioctlsocket((SOCKET)sockfd, FIONBIO, &mode);
#else
// On non-Windows, check if fd is too large for select()
if (sockfd >= FD_SETSIZE) {
close(sockfd);
continue;
}
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) flags = 0;
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
#endif
// try connect
int conn = connect(sockfd, p->ai_addr, (int)p->ai_addrlen);
#ifdef _WIN32
if (conn == 0) {
ok = true; // immediate success
}
else {
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK || err == WSAEINPROGRESS) {
// fall through to select
}
else {
// immediate failure
}
}
#else
if (conn == 0) {
ok = true; // immediate success
}
else {
if (errno == EINPROGRESS) {
// fall through to select
}
else {
// immediate failure
}
}
#endif
if (!ok) {
// wait for writable with timeout
fd_set writefds;
FD_ZERO(&writefds);
#ifdef _WIN32
FD_SET((SOCKET)sockfd, &writefds);
#else
FD_SET(sockfd, &writefds);
#endif
fd_set exceptfds;
FD_ZERO(&exceptfds);
#ifdef _WIN32
FD_SET((SOCKET)sockfd, &exceptfds);
#else
FD_SET(sockfd, &exceptfds);
#endif
timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
int sel = select(
#ifdef _WIN32
0,
#else
sockfd + 1,
#endif
nullptr, &writefds, &exceptfds, &tv);
if (sel > 0) {
// check for error on socket
int sock_err = 0;
socklen_t len = sizeof(sock_err);
#ifdef _WIN32
int ret = getsockopt((SOCKET)sockfd, SOL_SOCKET, SO_ERROR, (char*)&sock_err, &len);
#else
int ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &sock_err, &len);
#endif
#ifdef _WIN32
bool writable = FD_ISSET(static_cast<SOCKET>(sockfd), &writefds) != 0;
#else
bool writable = FD_ISSET(sockfd, &writefds) != 0;
#endif
if (ret == 0 && sock_err == 0 && writable) {
ok = true;
}
}
// else timeout or error -> try next addr
}
// close socket
#ifdef _WIN32
closesocket((SOCKET)sockfd);
#else
close(sockfd);
#endif
}
freeaddrinfo(res);
return ok;
}
// NOTE: Due to the nature of getaddrinfo, this can block indefinitely. Not good.
bool DNSResolve(const std::string &host, const std::string &service, addrinfo **res, std::string &error, DNSType type) {
#if PPSSPP_PLATFORM(SWITCH)
// Force IPv4 lookups.
if (type == DNSType::IPV6) {
*res = nullptr;
return false;
} else if (type == DNSType::ANY) {
type = DNSType::IPV4;
}
#endif
addrinfo hints = {0};
// TODO: Might be uses to lookup other values.
hints.ai_socktype = SOCK_STREAM;
#ifdef __ANDROID__
hints.ai_flags = AI_ADDRCONFIG;
#else
// AI_V4MAPPED seems to have issues on some platforms, not sure we should include it:
// http://stackoverflow.com/questions/1408030/what-is-the-purpose-of-the-ai-v4mapped-flag-in-getaddrinfo
hints.ai_flags = /*AI_V4MAPPED |*/ AI_ADDRCONFIG;
#endif
hints.ai_protocol = 0;
if (type == DNSType::IPV4)
hints.ai_family = AF_INET;
else if (type == DNSType::IPV6)
hints.ai_family = AF_INET6;
const char *servicep = service.empty() ? nullptr : service.c_str();
*res = nullptr;
int result = getaddrinfo(host.c_str(), servicep, &hints, res);
if (result == EAI_AGAIN) {
// Temporary failure. Since this already blocks, let's just try once more.
sleep_ms(1, "dns-resolve-poll");
result = getaddrinfo(host.c_str(), servicep, &hints, res);
}
if (result != 0) {
#ifdef _WIN32
error = ConvertWStringToUTF8(gai_strerror(result));
#else
error = gai_strerror(result);
#endif
if (*res != nullptr)
freeaddrinfo(*res);
*res = nullptr;
return false;
}
return true;
}
void DNSResolveFree(addrinfo *res) {
if (res)
freeaddrinfo(res);
}
bool GetLocalIP4List(std::vector<std::string> &IP4s) {
char ipstr[INET6_ADDRSTRLEN]; // We use IPv6 length since it's longer than IPv4
// getifaddrs first appeared in glibc 2.3, On Android officially supported since __ANDROID_API__ >= 24
#if defined(_IFADDRS_H_) || (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 3) || (__ANDROID_API__ >= 24)
INFO_LOG(Log::Net, "GetIPList from getifaddrs");
struct ifaddrs* ifAddrStruct = NULL;
struct ifaddrs* ifa = NULL;
getifaddrs(&ifAddrStruct);
if (ifAddrStruct != NULL) {
for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr) {
continue;
}
if (ifa->ifa_addr->sa_family == AF_INET) {
// is a valid IP4 Address
if (inet_ntop(AF_INET, &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr, ipstr, sizeof(ipstr)) != 0) {
IP4s.push_back(ipstr);
}
}
/*else if (ifa->ifa_addr->sa_family == AF_INET6) {
// is a valid IP6 Address
if (inet_ntop(AF_INET6, &((struct sockaddr_in6*)ifa->ifa_addr)->sin6_addr, ipstr, sizeof(ipstr)) != 0) {
IP6s.push_back(ipstr);
}
}*/
}
freeifaddrs(ifAddrStruct);
return true;
}
#elif defined(SIOCGIFCONF) // Better detection on Linux/UNIX/MacOS/some Android
INFO_LOG(Log::Net, "GetIPList from SIOCGIFCONF");
static struct ifreq ifreqs[32];
struct ifconf ifc{};
ifc.ifc_req = ifreqs;
ifc.ifc_len = sizeof(ifreqs);
int sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
ERROR_LOG(Log::Net, "GetIPList failed to create socket (result = %i, errno = %i)", sd, socket_errno);
return false;
}
int r = ioctl(sd, SIOCGIFCONF, (char*)&ifc);
if (r != 0) {
ERROR_LOG(Log::Net, "GetIPList failed ioctl/SIOCGIFCONF (result = %i, errno = %i)", r, socket_errno);
return false;
}
struct ifreq* item;
struct sockaddr* addr;
for (int i = 0; i < ifc.ifc_len / sizeof(struct ifreq); ++i)
{
item = &ifreqs[i];
addr = &(item->ifr_addr);
// Get the IP address
r = ioctl(sd, SIOCGIFADDR, item);
if (r != 0)
{
ERROR_LOG(Log::Net, "GetIPList failed ioctl/SIOCGIFADDR (i = %i, result = %i, errno = %i)", i, r, socket_errno);
}
if (ifreqs[i].ifr_addr.sa_family == AF_INET) {
// is a valid IP4 Address
if (inet_ntop(AF_INET, &((struct sockaddr_in*)addr)->sin_addr, ipstr, sizeof(ipstr)) != 0) {
IP4s.emplace_back(ipstr);
}
}
/*else if (ifreqs[i].ifr_addr.sa_family == AF_INET6) {
// is a valid IP6 Address
if (inet_ntop(AF_INET6, &((struct sockaddr_in6*)addr)->sin6_addr, ipstr, sizeof(ipstr)) != 0) {
IP6s.push_back(ipstr);
}
}*/
}
close(sd);
return true;
#else // Fallback to POSIX/Cross-platform way but may not work well on Linux (ie. only shows 127.0.0.1)
DEBUG_LOG(Log::Net, "GetIPList from fallback method");
struct addrinfo hints, * res, * p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
hints.ai_socktype = SOCK_DGRAM;
// Get local host name
char szHostName[256] = "";
if (::gethostname(szHostName, sizeof(szHostName))) {
// Error handling
}
int status;
if ((status = getaddrinfo(szHostName, NULL, &hints, &res)) != 0) {
//fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return false;
}
for (p = res; p != NULL; p = p->ai_next) {
if (p->ai_family == AF_INET) {
// is a valid IP4 Address
if (inet_ntop(p->ai_family, &(((struct sockaddr_in*)p->ai_addr)->sin_addr), ipstr, sizeof(ipstr)) != 0) {
IP4s.push_back(ipstr);
}
}
/*else if (p->ai_family == AF_INET6) {
// is a valid IP6 Address
if (inet_ntop(p->ai_family, &(((struct sockaddr_in6*)p->ai_addr)->sin6_addr), ipstr, sizeof(ipstr)) != 0) {
IP6s.push_back(ipstr);
}
}*/
}
freeaddrinfo(res); // free the linked list
return true;
#endif
return false;
}
// IP address parser
int inet_pton(int af, const char* src, void* dst)
{
if (af == AF_INET)
{
unsigned char *ip = (unsigned char *)dst;
int k = 0, x = 0;
char ch;
for (int i = 0; (ch = src[i]) != 0; i++)
{
if (ch == '.')
{
ip[k++] = x;
if (k == 4)
return 0;
x = 0;
}
else if (ch < '0' || ch > '9')
return 0;
else
x = x * 10 + ch - '0';
if (x > 255)
return 0;
}
ip[k++] = x;
if (k != 4)
return 0;
}
else if (af == AF_INET6)
{
unsigned short* ip = ( unsigned short* )dst;
int i;
for (i = 0; i < 8; i++) ip[i] = 0;
int k = 0;
unsigned int x = 0;
char ch;
int marknum = 0;
for (i = 0; src[i] != 0; i++)
{
if (src[i] == ':')
marknum++;
}
for (i = 0; (ch = src[i]) != 0; i++)
{
if (ch == ':')
{
x = ((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8);
ip[k++] = x;
if (k == 8)
return 0;
x = 0;
if (i > 0 && src[i - 1] == ':')
k += 7 - marknum;
}
else if (ch >= '0' && ch <= '9')
x = x * 16 + ch - '0';
else if (ch >= 'a' && ch <= 'f')
x = x * 16 + ch - 'a' + 10;
else if (ch >= 'A' && ch <= 'F')
x = x * 16 + ch - 'A' + 10;
else
return 0;
if (x > 0xFFFF)
return 0;
}
x = ((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8);
ip[k++] = x;
if (k != 8)
return 0;
}
return 1;
}
// Structs for implementing DNS are available here:
// https://web.archive.org/web/20201204080751/https://www.binarytides.com/dns-query-code-in-c-with-winsock/
#define DNS_PORT 53
#define DNS_QUERY_TYPE_A 1
#define DNS_QUERY_CLASS_IN 1
// DNS header structure
struct DNSHeader {
uint16_t id; // Identifier
uint16_t flags; // Flags
uint16_t q_count; // Number of questions
uint16_t ans_count; // Number of answers
uint16_t auth_count; // Number of authority records
uint16_t add_count; // Number of additional records
};
// Function to convert a domain name to DNS query format
// http://www.tcpipguide.com/free/t_DNSNameNotationandMessageCompressionTechnique.htm
static bool encode_domain_name(const char *domain, unsigned char *encoded, size_t max_len) {
const char *pos = domain;
unsigned char *ptr = encoded;
const unsigned char *end = encoded + max_len;
while (*pos) {
const char *start = pos;
while (*pos && *pos != '.') {
pos++;
}
size_t label_len = pos - start;
if (label_len > 63 || ptr + label_len + 1 >= end) {
return false; // Label too long or buffer overflow
}
*ptr++ = (unsigned char)label_len; // length field
memcpy(ptr, start, label_len);
ptr += label_len;
if (*pos == '.') {
pos++;
}
}
if (ptr >= end) {
return false;
}
*ptr = 0; // End of domain name
return true;
}
// Function to parse and print the DNS response
static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint32_t *output) {
if (response_len < sizeof(DNSHeader)) {
ERROR_LOG(Log::Net, "DNS response too short");
return false;
}
DNSHeader *dns = (DNSHeader *)buffer;
unsigned char *ptr = buffer + sizeof(struct DNSHeader);
unsigned char *end = buffer + response_len;
DEBUG_LOG(Log::Net, "DNS Response:");
DEBUG_LOG(Log::Net, "ID: 0x%x", ntohs(dns->id));
DEBUG_LOG(Log::Net, "Flags: 0x%x", ntohs(dns->flags));
DEBUG_LOG(Log::Net, "Questions: %d", ntohs(dns->q_count));
DEBUG_LOG(Log::Net, "Answers: %d", ntohs(dns->ans_count));
DEBUG_LOG(Log::Net, "Authority Records: %d", ntohs(dns->auth_count));
DEBUG_LOG(Log::Net, "Additional Records: %d", ntohs(dns->add_count));
// Skip over the question section
const int q_count = ntohs(dns->q_count);
for (int i = 0; i < q_count; i++) {
while (ptr < end && *ptr != 0) {
int jump = *ptr;
ptr += jump + 1;
if (ptr >= end) {
ERROR_LOG(Log::Net, "DNS response malformed (question section)");
return false;
}
}
ptr += 5; // Null byte + QTYPE (2 bytes) + QCLASS (2 bytes)
if (ptr > end) {
ERROR_LOG(Log::Net, "DNS response malformed (question section end)");
return false;
}
}
*output = 0;
// Parse the answer section
const int ans_count = ntohs(dns->ans_count);
for (int i = 0; i < ans_count; i++) {
DEBUG_LOG(Log::Net, "Answer %d:\n", i + 1);
// Skip the name (can be a pointer or a sequence)
if (ptr >= end) {
ERROR_LOG(Log::Net, "DNS response malformed (answer %d name)", i);
return false;
}
if ((*ptr & 0xC0) == 0xC0) {
if (ptr + 2 > end) {
ERROR_LOG(Log::Net, "DNS response malformed (answer %d name pointer)", i);
return false;
}
ptr += 2; // Pointer (2 bytes)
} else {
while (ptr < end && *ptr != 0) {
int jump = *ptr;
ptr += jump + 1;
if (ptr >= end) {
ERROR_LOG(Log::Net, "DNS response malformed (answer %d name loop)", i);
return false;
}
}
ptr++;
}
if (ptr + 10 > end) {
ERROR_LOG(Log::Net, "DNS response too short for answer %d header", i);
return false;
}
// TODO: Use a struct or something.
uint16_t type = ntohs(*((uint16_t *)ptr));
ptr += 2;
uint16_t clazz = ntohs(*((uint16_t *)ptr));
ptr += 2;
uint32_t ttl = ntohl(*((uint32_t *)ptr));
ptr += 4;
uint16_t data_len = ntohs(*((uint16_t *)ptr));
ptr += 2;
DEBUG_LOG(Log::Net, " Type: %d", type);
DEBUG_LOG(Log::Net, " Class: %d", clazz);
DEBUG_LOG(Log::Net, " TTL: %u", ttl);
DEBUG_LOG(Log::Net, " Data length: %d", (int)data_len);
if (ptr + data_len > end) {
ERROR_LOG(Log::Net, "DNS response data exceeds buffer");
return false;
}
if (type == DNS_QUERY_TYPE_A && data_len == 4) {
// IPv4 address
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, ptr, ip, sizeof(ip));
DEBUG_LOG(Log::Net, " IPV4 Address: %s", ip);
memcpy(output, ptr, 4);
// Skipping further responses.
return true;
}
ptr += data_len;
}
return false;
}
// This was written by ChatGPT, although not much of that remains, after all the cleanup and fixing...
// Specialized cache for the direct DNS lookups
struct DNSCacheEntry {
uint32_t ipv4Address;
};
static std::map<std::string, DNSCacheEntry> g_directDNSCache;
bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t *ipv4_addr) {
if (!strlen(dns_server_ip)) {
WARN_LOG(Log::Net, "Direct lookup: DNS server not specified");
return false;
}
if (!strlen(domain)) {
ERROR_LOG(Log::Net, "Direct lookup: Can't look up an empty domain");
return false;
}
std::string key = StringFromFormat("%s:%s", dns_server_ip, domain);
auto iter = g_directDNSCache.find(key);
if (iter != g_directDNSCache.end()) {
INFO_LOG(Log::Net, "Returning cached response from direct DNS request for '%s' to DNS server '%s", domain, dns_server_ip);
*ipv4_addr = iter->second.ipv4Address;
return true;
}
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// Create UDP socket
if (sockfd == INVALID_SOCKET) {
ERROR_LOG(Log::Net, "Socket creation for direct DNS failed");
return false;
}
#ifndef _WIN32
// On non-Windows, we can't use select() if fd >= FD_SETSIZE
// For DNS, just set a socket timeout instead
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
WARN_LOG(Log::Net, "Failed to set socket timeout for DNS query");
}
#else
// Windows version
DWORD timeout = 5000; // 5 seconds
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)) < 0) {
WARN_LOG(Log::Net, "Failed to set socket timeout for DNS query");
}
#endif
struct sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(DNS_PORT);
if (net::inet_pton(AF_INET, dns_server_ip, &server_addr.sin_addr) <= 0) {
ERROR_LOG(Log::Net,"Invalid DNS server IP address %s", dns_server_ip);
closesocket(sockfd);
return false;
}
// Build DNS query
unsigned char buffer[1024]{};
struct DNSHeader *dns = (struct DNSHeader *)buffer;
dns->id = htons(0x1234); // Random ID
dns->flags = htons(0x0100); // Standard query
dns->q_count = htons(1); // One question
unsigned char *qname = buffer + sizeof(DNSHeader);
size_t qname_space = sizeof(buffer) - sizeof(DNSHeader) - 4; // Reserve 4 bytes for qtype and qclass
if (!encode_domain_name(domain, qname, qname_space)) {
ERROR_LOG(Log::Net, "Domain name too long or invalid: %s", domain);
closesocket(sockfd);
return false;
}
unsigned char *qinfo = qname + strlen((const char *)qname) + 1;
*((uint16_t *)qinfo) = htons(DNS_QUERY_TYPE_A); // Query type: A
*((uint16_t *)(qinfo + 2)) = htons(DNS_QUERY_CLASS_IN); // Query class: IN
// Send DNS query
size_t query_len = (qinfo + 4) - buffer;
if (sendto(sockfd, (const char *)buffer, (int)query_len, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
ERROR_LOG(Log::Net, "Failed to send DNS query");
closesocket(sockfd);
return false;
}
// Receive DNS response
socklen_t server_len = sizeof(server_addr);
int response_len = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &server_len);
if (response_len < 0) {
ERROR_LOG(Log::Net, "Failed to receive DNS response (timeout or error)");
closesocket(sockfd);
return false;
}
// Close socket
closesocket(sockfd);
// Done communicating, time to parse.
if (!parse_dns_response(buffer, (size_t)response_len, ipv4_addr)) {
return false;
}
g_directDNSCache[key] = DNSCacheEntry{ *ipv4_addr };
return true;
}
} // namespace net