/* RetroArch - A frontend for libretro. * Copyright (C) 2021-2022 - Roberto V. Rampim * * 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 . */ #include #include #include #include #include #include #if !defined(HAVE_SOCKET_LEGACY) || defined(GEKKO) #include #endif #include "../tasks/tasks_internal.h" #include "natt.h" static bool translate_addr(struct sockaddr_in *addr, char *host, size_t hostlen, char *port, size_t portlen) { #ifndef HAVE_SOCKET_LEGACY if (getnameinfo((struct sockaddr *) addr, sizeof(*addr), host, hostlen, port, portlen, NI_NUMERICHOST | NI_NUMERICSERV)) return false; #else /* We need to do the conversion/translation manually. */ { int res; uint8_t *addr8 = (uint8_t *) &addr->sin_addr; uint16_t port16 = ntohs(addr->sin_port); if (host) { res = snprintf(host, hostlen, "%d.%d.%d.%d", (int) addr8[0], (int) addr8[1], (int) addr8[2], (int) addr8[3]); if (res < 0 || res >= hostlen) return false; } if (port) { res = snprintf(port, portlen, "%hu", port16); if (res < 0 || res >= portlen) return false; } } #endif return true; } 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; 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); if (fd < 0) goto failure; if (!bind_addr) goto failure; #if !defined(HAVE_SOCKET_LEGACY) || defined(GEKKO) { 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) return false; if (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'; device->busy = false; 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(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 bool build_control_url(rxml_node_t *control_url, struct natt_device *device) { if (string_is_empty(control_url->data)) return false; /* Do we already have the full url? */ if (string_starts_with_case_insensitive(control_url->data, "http://")) { /* Make sure the control URL isn't too long. */ if (strlcpy(device->control, control_url->data, sizeof(device->control)) >= sizeof(device->control)) { *device->control = '\0'; return false; } } else { /* We don't have a full url. Build one using the desc url. */ char *control_path; strlcpy(device->control, device->desc, sizeof(device->control)); control_path = (char *) strchr(device->control + STRLEN_CONST("http://"), '/'); if (control_path) *control_path = '\0'; if (control_url->data[0] != '/') strlcat(device->control, "/", sizeof(device->control)); /* Make sure the control URL isn't too long. */ if (strlcat(device->control, control_url->data, sizeof(device->control)) >= sizeof(device->control)) { *device->control = '\0'; return false; } } return true; } static bool parse_desc_node(rxml_node_t *node, struct natt_device *device) { rxml_node_t *child = node->children; if (!child) return false; /* We only care for services. */ if (string_is_equal_case_insensitive(node->name, "service")) { rxml_node_t *service_type = NULL; rxml_node_t *control_url = NULL; do { if (string_is_equal_case_insensitive(child->name, "serviceType")) service_type = child; else if (string_is_equal_case_insensitive(child->name, "controlURL")) control_url = child; if (service_type && control_url) break; } while ((child = child->next)); if (!service_type || !control_url) return false; /* These two are the only IGD service types we can work with. */ if (!strstr(service_type->data, ":WANIPConnection:") && !strstr(service_type->data, ":WANPPPConnection:")) return false; if (!build_control_url(control_url, device)) return false; strlcpy(device->service_type, service_type->data, sizeof(device->service_type)); return true; } /* XML recursion */ do { if (parse_desc_node(child, device)) return true; } while ((child = child->next)); return false; } static void natt_query_device_cb(retro_task_t *task, void *task_data, void *user_data, const char *error) { 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 (error) 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. */ document = rxml_load_document_string(xml); if (document) { rxml_node_t *root = rxml_root_node(document); if (root) parse_desc_node(root, device); rxml_free_document(document); } free(xml); done: device->busy = false; } bool natt_query_device(struct natt_device *device, bool block) { if (!device) return false; if (string_is_empty(device->desc)) return false; if (device->busy) return false; device->busy = true; if (!task_push_http_transfer(device->desc, true, NULL, natt_query_device_cb, device)) { device->busy = false; return false; } if (block) task_queue_wait(NULL, NULL); return true; } static bool 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 (string_is_empty(node->data)) return false; hints.ai_family = AF_INET; 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 (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 *error) { 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 (error) 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. */ document = rxml_load_document_string(xml); if (document) { rxml_node_t *root = rxml_root_node(document); if (root) parse_external_address_node(root, device); rxml_free_document(document); } free(xml); done: device->busy = false; } static bool 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 (string_is_empty(node->data)) return false; sscanf(node->data, "%hu", &ext_port); if (!ext_port) return false; 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 (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 *error) { 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 (error) 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. */ document = rxml_load_document_string(xml); if (document) { rxml_node_t *root = rxml_root_node(document); if (root) parse_open_port_node(root, request); rxml_free_document(document); } free(xml); done: device->busy = false; } static void natt_close_port_cb(retro_task_t *task, void *task_data, void *user_data, const char *error) { 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 (error) 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: device->busy = false; } 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 (string_is_empty(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, bool block) { static const char tmpl[] = "" "" "" "" "" ""; char buf[1024]; if (!device) return false; snprintf(buf, sizeof(buf), tmpl, device->service_type); if (device->busy) return false; device->busy = true; if (!natt_action(device, "GetExternalIPAddress", buf, natt_external_address_cb, NULL)) { device->busy = false; return false; } if (block) task_queue_wait(NULL, NULL); return true; } bool natt_open_port(struct natt_device *device, struct natt_request *request, enum natt_forward_type forward_type, bool block) { static const char tmpl[] = "" "" "" "" "" "%s" "%s" "%s" "%s" "1" "retroarch" "0" "" "" ""; 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 (!translate_addr(&request->addr, host, sizeof(host), port, sizeof(port))) 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 (device->busy) return false; device->busy = true; if (!natt_action(device, action, buf, natt_open_port_cb, request)) { device->busy = false; return false; } if (block) task_queue_wait(NULL, NULL); return true; } bool natt_close_port(struct natt_device *device, struct natt_request *request, bool block) { static const char tmpl[] = "" "" "" "" "" "%s" "%s" "" "" ""; char buf[1024]; const char *protocol; char port[6]; if (!device || !request) return false; if (!request->addr.sin_port) return false; if (!translate_addr(&request->addr, NULL, 0, port, sizeof(port))) return false; protocol = (request->proto == SOCKET_PROTOCOL_UDP) ? "UDP" : "TCP"; snprintf(buf, sizeof(buf), tmpl, device->service_type, port, protocol); if (device->busy) return false; device->busy = true; if (!natt_action(device, "DeletePortMapping", buf, natt_close_port_cb, request)) { device->busy = false; return false; } if (block) task_queue_wait(NULL, NULL); return true; }