Files
RetroArch/network/natt.c
T
LibretroAdmin e8dc09f986 natt: hand the UPnP reply to the XML document instead of copying it twice
Each of the three UPnP replies (device description, external address,
port mapping) was copied into a NUL-terminated heap buffer, then
copied again by rxml_load_document_string, which keeps its own.  The
buffer is now handed over with rxml_load_document_owned, which keeps it
as the document's or frees it on failure, so the second copy and its
allocation are gone and the caller no longer frees it.

natt_desc_parse_test passes.
2026-11-07 06:36:57 +01:00

651 lines
17 KiB
C

/* RetroArch - A frontend for libretro.
* Copyright (C) 2021-2022 - Libretro team
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <stdio.h>
#include <string/stdstring.h>
#include <formats/rxml.h>
#include <features/features_cpu.h>
#include <retro_miscellaneous.h>
#ifdef HAVE_IFINFO
#include <net/net_ifinfo.h>
#endif
#include "../tasks/tasks_internal.h"
#include "natt.h"
#include <compat/strl.h>
#include "natt_desc.h"
bool natt_init(struct natt_discovery *discovery)
{
static const char msearch[] =
"M-SEARCH * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"MAN: \"ssdp:discover\"\r\n"
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n"
"MX: 5\r\n"
"\r\n";
bool ret;
int fd = -1;
struct addrinfo *msearch_addr = NULL;
struct addrinfo *bind_addr = NULL;
struct addrinfo hints = {0};
if (!discovery)
return false;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
if (getaddrinfo_retro("239.255.255.250", "1900", &hints, &msearch_addr))
goto failure;
if (!msearch_addr)
goto failure;
fd = socket_init((void**)&bind_addr, 0, NULL,
SOCKET_TYPE_DATAGRAM, AF_INET);
if (fd < 0)
goto failure;
if (!bind_addr)
goto failure;
#ifdef HAVE_IFINFO
{
struct sockaddr_in *addr = (struct sockaddr_in *) bind_addr->ai_addr;
if (net_ifinfo_best("223.255.255.255", &addr->sin_addr, false))
{
#ifdef IP_MULTICAST_IF
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF,
(const char *) &addr->sin_addr, sizeof(addr->sin_addr));
#endif
}
}
#endif
#ifdef IP_MULTICAST_TTL
{
#ifdef _WIN32
unsigned long ttl = 2;
if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL,
(const char *) &ttl, sizeof(ttl)) < 0) { }
#else
unsigned char ttl = 2;
if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL,
&ttl, sizeof(ttl)) < 0) { }
#endif
}
#endif
if (!socket_bind(fd, bind_addr))
goto failure;
if (sendto(fd, msearch, STRLEN_CONST(msearch), 0,
msearch_addr->ai_addr, msearch_addr->ai_addrlen)
!= STRLEN_CONST(msearch))
goto failure;
if (!socket_nonblock(fd))
goto failure;
discovery->fd = fd;
discovery->timeout = cpu_features_get_time_usec() + 5000000;
ret = true;
goto done;
failure:
if (fd >= 0)
socket_close(fd);
discovery->fd = -1;
discovery->timeout = -1;
ret = false;
done:
freeaddrinfo_retro(msearch_addr);
freeaddrinfo_retro(bind_addr);
return ret;
}
bool natt_device_next(struct natt_discovery *discovery,
struct natt_device *device)
{
char buf[2048];
ssize_t recvd;
char *data;
size_t remaining;
struct sockaddr_storage addr = {0};
socklen_t addr_size = sizeof(addr);
if (!discovery || !device || discovery->fd < 0)
return false;
/* This is faster than memsetting the whole thing. */
memset(&device->addr, 0, sizeof(device->addr));
memset(&device->ext_addr, 0, sizeof(device->ext_addr));
*device->desc = '\0';
*device->control = '\0';
*device->service_type = '\0';
retro_atomic_store_release_int(&device->busy, 0);
recvd = recvfrom(discovery->fd, buf, sizeof(buf), 0,
(struct sockaddr*)&addr, &addr_size);
if (recvd < 0)
{
/* If there was no data, check for timeout. */
if (isagain((int)recvd))
return cpu_features_get_time_usec() < discovery->timeout;
return false;
}
/* Zero-length datagrams are valid, but we can't do anything with them.
Don't treat them as an error. */
if (!recvd)
return true;
/* Make sure we've an IPv4. */
if (!addr_6to4(&addr))
return true;
memcpy(&device->addr, &addr, sizeof(device->addr));
/* Parse the data we received.
We are only looking for the 'Location' HTTP header. */
data = buf;
remaining = (size_t)recvd;
do
{
char *lnbreak = (char*)memchr(data, '\n', remaining);
if (!lnbreak)
break;
*lnbreak++ = '\0';
/* This also gets rid of any trailing carriage return. */
string_trim_whitespace_right(data);
string_trim_whitespace_left(data);
if (string_starts_with_case_insensitive(data, "Location:"))
{
char *location = string_trim_whitespace_left(
data + STRLEN_CONST("Location:"));
if (string_starts_with_case_insensitive(location, "http://"))
{
/* Make sure the description URL isn't too long. */
if (strlcpy(device->desc, location, sizeof(device->desc)) <
sizeof(device->desc))
return true;
*device->desc = '\0';
}
}
remaining -= (size_t)lnbreak - (size_t)data;
data = lnbreak;
} while (remaining);
/* This is not a failure.
We just don't yet have a valid device to report. */
return true;
}
void natt_device_end(struct natt_discovery *discovery)
{
if (discovery)
{
if (discovery->fd >= 0)
socket_close(discovery->fd);
discovery->fd = -1;
discovery->timeout = -1;
}
}
static void natt_query_device_cb(retro_task_t *task, void *task_data,
void *user_data, const char *err)
{
char *xml = NULL;
rxml_document_t *document = NULL;
http_transfer_data_t *data = (http_transfer_data_t*)task_data;
struct natt_device *device = (struct natt_device*)user_data;
*device->control = '\0';
*device->service_type = '\0';
if (err)
goto done;
if (!data || !data->data || !data->len)
goto done;
if (data->status != 200)
goto done;
xml = (char*)malloc(data->len + 1);
if (!xml)
goto done;
memcpy(xml, data->data, data->len);
xml[data->len] = '\0';
/* Parse the device's description XML. */
/* The document takes the copy over (and frees it on failure)
* rather than copying it a second time. */
document = rxml_load_document_owned(xml, data->len);
if (document)
{
rxml_node_t *root = rxml_root_node(document);
if (root)
natt_parse_desc_node(root, device);
rxml_free_document(document);
}
done:
retro_atomic_store_release_int(&device->busy, 0);
task_queue_wake(device->waiter);
}
bool natt_query_device(struct natt_device *device)
{
if ( !device || !*device->desc
|| retro_atomic_load_acquire_int(&device->busy))
return false;
retro_atomic_store_release_int(&device->busy, 1);
if (!task_push_http_transfer(device->desc,
true, NULL, natt_query_device_cb, device))
{
retro_atomic_store_release_int(&device->busy, 0);
return false;
}
return true;
}
static bool natt_parse_external_address_node(rxml_node_t *node,
struct natt_device *device)
{
if (string_is_equal_case_insensitive(node->name, "NewExternalIPAddress"))
{
struct addrinfo *addr = NULL;
struct addrinfo hints = {0};
if (!node->data || !*node->data)
return false;
hints.ai_family = AF_INET;
hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
if (getaddrinfo_retro(node->data, "0", &hints, &addr))
return false;
if (!addr)
return false;
memcpy(&device->ext_addr, addr->ai_addr,
sizeof(device->ext_addr));
freeaddrinfo_retro(addr);
return true;
}
else
{
/* XML recursion */
rxml_node_t *child = node->children;
if (child)
{
do
{
if (natt_parse_external_address_node(child, device))
return true;
} while ((child = child->next));
}
}
return false;
}
static void natt_external_address_cb(retro_task_t *task, void *task_data,
void *user_data, const char *err)
{
char *xml = NULL;
rxml_document_t *document = NULL;
http_transfer_data_t *data = (http_transfer_data_t*)task_data;
struct natt_device *device = (struct natt_device*)user_data;
memset(&device->ext_addr, 0, sizeof(device->ext_addr));
if (err)
goto done;
if (!data || !data->data || !data->len)
goto done;
if (data->status != 200)
goto done;
xml = (char*)malloc(data->len + 1);
if (!xml)
goto done;
memcpy(xml, data->data, data->len);
xml[data->len] = '\0';
/* Parse the returned external ip address. */
/* The document takes the copy over (and frees it on failure)
* rather than copying it a second time. */
document = rxml_load_document_owned(xml, data->len);
if (document)
{
rxml_node_t *root = rxml_root_node(document);
if (root)
natt_parse_external_address_node(root, device);
rxml_free_document(document);
}
done:
retro_atomic_store_release_int(&device->busy, 0);
task_queue_wake(device->waiter);
}
static bool natt_parse_open_port_node(rxml_node_t *node,
struct natt_request *request)
{
if (string_is_equal_case_insensitive(node->name, "u:AddPortMappingResponse"))
{
request->success = true;
return true;
}
else if (string_is_equal_case_insensitive(node->name, "NewReservedPort"))
{
uint16_t ext_port = 0;
if (!node->data || !*node->data)
return false;
{
unsigned long tmp = strtoul(node->data, NULL, 10);
if (tmp == 0 || tmp > 0xFFFF)
return false;
ext_port = (uint16_t)tmp;
}
request->addr.sin_port = htons(ext_port);
request->success = true;
return true;
}
else
{
/* XML recursion */
rxml_node_t *child = node->children;
if (child)
{
do
{
if (natt_parse_open_port_node(child, request))
return true;
} while ((child = child->next));
}
}
return false;
}
static void natt_open_port_cb(retro_task_t *task, void *task_data,
void *user_data, const char *err)
{
char *xml = NULL;
rxml_document_t *document = NULL;
http_transfer_data_t *data = (http_transfer_data_t*)task_data;
struct natt_request *request = (struct natt_request*)user_data;
struct natt_device *device = (struct natt_device*)request->device;
request->success = false;
if (err)
goto done;
if (!data || !data->data || !data->len)
goto done;
if (data->status != 200)
goto done;
xml = (char*)malloc(data->len + 1);
if (!xml)
goto done;
memcpy(xml, data->data, data->len);
xml[data->len] = '\0';
/* Parse the device's port forwarding response. */
/* The document takes the copy over (and frees it on failure)
* rather than copying it a second time. */
document = rxml_load_document_owned(xml, data->len);
if (document)
{
rxml_node_t *root = rxml_root_node(document);
if (root)
natt_parse_open_port_node(root, request);
rxml_free_document(document);
}
done:
retro_atomic_store_release_int(&device->busy, 0);
task_queue_wake(device->waiter);
}
static void natt_close_port_cb(retro_task_t *task, void *task_data,
void *user_data, const char *err)
{
http_transfer_data_t *data = (http_transfer_data_t*)task_data;
struct natt_request *request = (struct natt_request*)user_data;
struct natt_device *device = (struct natt_device*)request->device;
request->success = false;
if (err)
goto done;
if (!data || !data->data || !data->len)
goto done;
if (data->status != 200)
goto done;
/* We don't need to do anything special here. */
request->success = true;
done:
retro_atomic_store_release_int(&device->busy, 0);
task_queue_wake(device->waiter);
}
static bool natt_action(struct natt_device *device,
const char *action, const char *data, retro_task_callback_t cb,
struct natt_request *request)
{
static const char headers_tmpl[] =
"Content-Type: text/xml\r\n"
"SOAPAction: \"%s#%s\"\r\n";
char headers[512];
void *obj;
if (!*device->control)
return false;
snprintf(headers, sizeof(headers), headers_tmpl,
device->service_type, action);
if (request)
{
request->device = device;
obj = request;
}
else
obj = device;
return task_push_http_post_transfer_with_headers(device->control,
data, true, NULL, headers, cb, obj) != NULL;
}
bool natt_external_address(struct natt_device *device)
{
static const char tmpl[] =
"<?xml version=\"1.0\"?>"
"<s:Envelope "
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" "
"xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\""
">"
"<s:Body>"
"<u:GetExternalIPAddress xmlns:u=\"%s\"/>"
"</s:Body>"
"</s:Envelope>";
char buf[1024];
if (!device)
return false;
snprintf(buf, sizeof(buf), tmpl,
device->service_type);
if (retro_atomic_load_acquire_int(&device->busy))
return false;
retro_atomic_store_release_int(&device->busy, 1);
if (!natt_action(device, "GetExternalIPAddress", buf,
natt_external_address_cb, NULL))
{
retro_atomic_store_release_int(&device->busy, 0);
return false;
}
return true;
}
bool natt_open_port(struct natt_device *device,
struct natt_request *request, enum natt_forward_type forward_type)
{
static const char tmpl[] =
"<?xml version=\"1.0\"?>"
"<s:Envelope "
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" "
"xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\""
">"
"<s:Body>"
"<u:%s xmlns:u=\"%s\">"
"<NewRemoteHost></NewRemoteHost>"
"<NewExternalPort>%s</NewExternalPort>"
"<NewProtocol>%s</NewProtocol>"
"<NewInternalPort>%s</NewInternalPort>"
"<NewInternalClient>%s</NewInternalClient>"
"<NewEnabled>1</NewEnabled>"
"<NewPortMappingDescription>retroarch</NewPortMappingDescription>"
"<NewLeaseDuration>0</NewLeaseDuration>"
"</u:%s>"
"</s:Body>"
"</s:Envelope>";
char buf[1280];
const char *action, *protocol;
char host[256], port[6];
if (!device || !request)
return false;
if (!request->addr.sin_port)
return false;
if (getnameinfo_retro((struct sockaddr*)&request->addr,
sizeof(request->addr), host, sizeof(host), port, sizeof(port),
NI_NUMERICHOST | NI_NUMERICSERV))
return false;
action = (forward_type == NATT_FORWARD_TYPE_ANY) ?
"AddAnyPortMapping" : "AddPortMapping";
protocol = (request->proto == SOCKET_PROTOCOL_UDP) ?
"UDP" : "TCP";
snprintf(buf, sizeof(buf), tmpl,
action, device->service_type,
port, protocol, port, host,
action);
if (retro_atomic_load_acquire_int(&device->busy))
return false;
retro_atomic_store_release_int(&device->busy, 1);
if (!natt_action(device, action, buf,
natt_open_port_cb, request))
{
retro_atomic_store_release_int(&device->busy, 0);
return false;
}
return true;
}
bool natt_close_port(struct natt_device *device,
struct natt_request *request)
{
static const char tmpl[] =
"<?xml version=\"1.0\"?>"
"<s:Envelope "
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" "
"xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\""
">"
"<s:Body>"
"<u:DeletePortMapping xmlns:u=\"%s\">"
"<NewRemoteHost></NewRemoteHost>"
"<NewExternalPort>%s</NewExternalPort>"
"<NewProtocol>%s</NewProtocol>"
"</u:DeletePortMapping>"
"</s:Body>"
"</s:Envelope>";
char buf[1024];
const char *protocol;
char port[6];
if (!device || !request)
return false;
if (!request->addr.sin_port)
return false;
if (getnameinfo_retro((struct sockaddr*)&request->addr,
sizeof(request->addr), NULL, 0, port, sizeof(port), NI_NUMERICSERV))
return false;
protocol = (request->proto == SOCKET_PROTOCOL_UDP) ?
"UDP" : "TCP";
snprintf(buf, sizeof(buf), tmpl,
device->service_type, port, protocol);
if (retro_atomic_load_acquire_int(&device->busy))
return false;
retro_atomic_store_release_int(&device->busy, 1);
if (!natt_action(device, "DeletePortMapping", buf,
natt_close_port_cb, request))
{
retro_atomic_store_release_int(&device->busy, 0);
return false;
}
return true;
}