diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a7339c4b..c481a4cfb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -297,7 +297,8 @@ endif () target_link_libraries(moonlight-lib PUBLIC commons-ss4s-modules-list commons-sps-parser) target_link_libraries(moonlight-lib PUBLIC commons-logging commons-gamecontrollerdb-updater commons-lazy - commons-refcounter commons-executor commons-linked-list commons-wol commons-ini-writer commons-copyfile) + commons-refcounter commons-executor commons-linked-list commons-wol commons-ini-writer commons-copyfile + commons-sockaddr) configure_file(src/app/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY) target_include_directories(moonlight-lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/core/libgamestream/libgamestream/client.h b/core/libgamestream/libgamestream/client.h index 1b047a3b6..1101fc353 100644 --- a/core/libgamestream/libgamestream/client.h +++ b/core/libgamestream/libgamestream/client.h @@ -33,6 +33,10 @@ typedef struct _SERVER_DATA { const char *mac; const char *hostname; const char *gpuType; + /** HttpsPort */ + unsigned short httpsPort; + /** ExternalPort */ + unsigned short extPort; bool paired; bool supports4K; bool supportsHdr; @@ -55,7 +59,7 @@ void gs_destroy(GS_CLIENT hnd); void gs_set_timeout(GS_CLIENT hnd, int timeout_secs); -int gs_get_status(GS_CLIENT hnd, PSERVER_DATA server, const char *address, bool unsupported); +int gs_get_status(GS_CLIENT hnd, PSERVER_DATA server, const char *address, uint16_t port, bool unsupported); int gs_start_app(GS_CLIENT hnd, PSERVER_DATA server, PSTREAM_CONFIGURATION config, int appId, bool is_gfe, bool sops, bool localaudio, int gamepad_mask); diff --git a/core/libgamestream/src/client.c b/core/libgamestream/src/client.c index 8cee14b64..259a12a3a 100644 --- a/core/libgamestream/src/client.c +++ b/core/libgamestream/src/client.c @@ -59,156 +59,16 @@ #include "set_error.h" #include "conf.h" -static bool construct_url(GS_CLIENT, char *url, size_t ulen, bool secure, const char *address, const char *action, - const char *fmt, ...); +static int load_server_status(GS_CLIENT hnd, PSERVER_DATA server); -static bool append_param(char *url, size_t ulen, const char *param, const char *value_fmt, ...); - -static int load_server_status(GS_CLIENT hnd, PSERVER_DATA server) { - int ret; - char url[4096]; - int i; - - i = 0; - do { - char *pairedText = NULL; - char *currentGameText = NULL; - char *stateText = NULL; - char *serverCodecModeSupportText = NULL; - - ret = GS_INVALID; - - // Modern GFE versions don't allow serverinfo to be fetched over HTTPS if the client - // is not already paired. Since we can't pair without knowing the server version, we - // make another request over HTTP if the HTTPS request fails. We can't just use HTTP - // for everything because it doesn't accurately tell us if we're paired. - construct_url(hnd, url, sizeof(url), i == 0, server->serverInfo.address, "serverinfo", NULL); - - HTTP_DATA *data = http_data_alloc(); - if (data == NULL) { - ret = GS_OUT_OF_MEMORY; - goto cleanup; - } - if (http_request(hnd->http, url, data) != GS_OK) { - ret = GS_IO_ERROR; - goto cleanup; - } - - if (xml_status(data->memory, data->size) == GS_ERROR) { - ret = GS_ERROR; - goto cleanup; - } - - if (xml_search(data->memory, data->size, "uniqueid", (char **) &server->uuid) != GS_OK) { - goto cleanup; - } - if (xml_search(data->memory, data->size, "mac", (char **) &server->mac) != GS_OK) { - goto cleanup; - } - if (xml_search(data->memory, data->size, "hostname", (char **) &server->hostname) != GS_OK) { - server->hostname = strdup(server->serverInfo.address); - } - - if (xml_search(data->memory, data->size, "currentgame", ¤tGameText) != GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "PairStatus", &pairedText) != GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "appversion", (char **) &server->serverInfo.serverInfoAppVersion) != - GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "state", &stateText) != GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "ServerCodecModeSupport", &serverCodecModeSupportText) != GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "gputype", (char **) &server->gpuType) != GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "GsVersion", (char **) &server->gsVersion) != GS_OK) { - goto cleanup; - } - - if (xml_search(data->memory, data->size, "GfeVersion", (char **) &server->serverInfo.serverInfoGfeVersion) != - GS_OK) { - goto cleanup; - } - - if (xml_modelist(data->memory, data->size, &server->modes) != GS_OK) { - goto cleanup; - } +static int resolve_ports(GS_CLIENT hnd, const char *address, uint16_t port, uint16_t *https_port); - // These fields are present on all version of GFE that this client supports - if (!strlen(currentGameText) || !strlen(pairedText) || !strlen(server->serverInfo.serverInfoAppVersion) || - !strlen(stateText)) { - goto cleanup; - } - - int serverCodecModeSupport = serverCodecModeSupportText == NULL ? 0 : atoi(serverCodecModeSupportText); - - server->paired = pairedText != NULL && strcmp(pairedText, "1") == 0; - server->currentGame = currentGameText == NULL ? 0 : atoi(currentGameText); - server->supports4K = serverCodecModeSupport != 0; - server->supportsHdr = serverCodecModeSupport & 0x200; - server->serverMajorVersion = atoi(server->serverInfo.serverInfoAppVersion); - // Real Nvidia host software (GeForce Experience and RTX Experience) both use the 'Mjolnir' - // codename in the state field and no version of Sunshine does. We can use this to bypass - // some assumptions about Nvidia hardware that don't apply to Sunshine hosts. - server->isGfe = strstr(stateText, "MJOLNIR") != NULL; - - if (strstr(stateText, "_SERVER_BUSY") == NULL) { - // After GFE 2.8, current game remains set even after streaming - // has ended. We emulate the old behavior by forcing it to zero - // if streaming is not active. - server->currentGame = 0; - } - ret = GS_OK; - - cleanup: - if (data != NULL) { - http_data_free(data); - } - - if (pairedText != NULL) { - free(pairedText); - } - - if (currentGameText != NULL) { - free(currentGameText); - } - - if (stateText != NULL) { - free(stateText); - } - - if (serverCodecModeSupportText != NULL) { - free(serverCodecModeSupportText); - } - - i++; - } while (ret != GS_OK && i < 2); +static bool construct_url(GS_CLIENT, char *url, size_t ulen, bool secure, const char *address, uint16_t port, + const char *action, const char *fmt, ...); - if (ret == GS_OK && !server->unsupported) { - if (server->serverMajorVersion > MAX_SUPPORTED_GFE_VERSION) { - ret = gs_set_error(GS_UNSUPPORTED_VERSION, "Ensure you're running the latest version of Moonlight " - "or downgrade GeForce Experience and try again"); - } else if (server->serverMajorVersion < MIN_SUPPORTED_GFE_VERSION) { - ret = gs_set_error(GS_UNSUPPORTED_VERSION, "Moonlight requires a newer version of GeForce Experience. " - "Please upgrade GFE on your PC and try again."); - } - } +static bool append_param(char *url, size_t ulen, const char *param, const char *value_fmt, ...); - return ret; -} +static uint16_t server_port(const SERVER_DATA *server, bool secure); static void bytes_to_hex(const unsigned char *in, char *out, size_t len) { for (int i = 0; i < len; i++) { @@ -245,7 +105,7 @@ int gs_unpair(GS_CLIENT hnd, PSERVER_DATA server) { return GS_OUT_OF_MEMORY; } - construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, "unpair", NULL); + construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, server_port(server, false), "unpair", NULL); ret = http_request(hnd->http, url, data); if (ret != GS_OK) { goto cleanup; @@ -338,7 +198,7 @@ int gs_pair(GS_CLIENT hnd, PSERVER_DATA server, const char *pin) { // Send the salt and get the server cert. This doesn't have a read timeout // because the user must enter the PIN before the server responds - construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, "pair", + construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, server_port(server, false), "pair", "devicename=roth&updateState=1&phrase=getservercert&salt=%s&clientcert=%s", salt_hex, hnd->cert_hex); data = http_data_alloc(); @@ -405,7 +265,7 @@ int gs_pair(GS_CLIENT hnd, PSERVER_DATA server, const char *pin) { bytes_to_hex(encrypted_challenge, encrypted_challenge_hex, 16); // Send the encrypted challenge to the server - construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, "pair", + construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, server_port(server, false), "pair", "devicename=roth&updateState=1&clientchallenge=%s", encrypted_challenge_hex); if ((ret = http_request(hnd->http, url, data)) != GS_OK) { goto cleanup; @@ -467,7 +327,7 @@ int gs_pair(GS_CLIENT hnd, PSERVER_DATA server, const char *pin) { char challenge_response_hex[65]; bytes_to_hex(challenge_response_encrypted, challenge_response_hex, 32); - construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, "pair", + construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, server_port(server, false), "pair", "devicename=rothupdateState=1&serverchallengeresp=%s", challenge_response_hex); if ((ret = http_request(hnd->http, url, data)) != GS_OK) { goto cleanup; @@ -537,7 +397,7 @@ int gs_pair(GS_CLIENT hnd, PSERVER_DATA server, const char *pin) { char client_pairing_secret_hex[sizeof(client_pairing_secret) * 2 + 1]; bytes_to_hex((unsigned char *) &client_pairing_secret, client_pairing_secret_hex, sizeof(client_pairing_secret)); - construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, "pair", + construct_url(hnd, url, sizeof(url), false, server->serverInfo.address, server_port(server, false), "pair", "devicename=roth&updateState=1&clientpairingsecret=%s", client_pairing_secret_hex); if ((ret = http_request(hnd->http, url, data)) != GS_OK) { goto cleanup; @@ -558,7 +418,7 @@ int gs_pair(GS_CLIENT hnd, PSERVER_DATA server, const char *pin) { } // Do the initial challenge (seems neccessary for us to show as paired) - construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, "pair", + construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, server_port(server, true), "pair", "devicename=roth&updateState=1&phrase=pairchallenge"); if ((ret = http_request(hnd->http, url, data)) != GS_OK) { goto cleanup; @@ -610,7 +470,7 @@ int gs_applist(GS_CLIENT hnd, const SERVER_DATA *server, PAPP_LIST *list) { return gs_set_error(GS_OUT_OF_MEMORY, "Out of memory"); } - construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, "applist", NULL); + construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, server_port(server, true), "applist", NULL); if (http_request(hnd->http, url, data) != GS_OK) { ret = gs_set_error(GS_IO_ERROR, "Failed to get apps list"); } else if (xml_status(data->memory, data->size) == GS_ERROR) { @@ -687,7 +547,8 @@ int gs_start_app(GS_CLIENT hnd, PSERVER_DATA server, STREAM_CONFIGURATION *confi int surround_info = SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config->audioConfiguration); const char *action = resume ? "launch" : "resume"; - construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, action, "appid=%d", appId); + construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, server_port(server, true), action, + "appid=%d", appId); append_param(url, sizeof(url), "mode", "%dx%dx%d", config->width, config->height, fps); append_param(url, sizeof(url), "additionalStates", "1"); @@ -754,7 +615,7 @@ int gs_quit_app(GS_CLIENT hnd, PSERVER_DATA server) { return gs_set_error(GS_OUT_OF_MEMORY, "Out of memory"); } - construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, "cancel", NULL); + construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, server_port(server, true), "cancel", NULL); if ((ret = http_request(hnd->http, url, data)) != GS_OK) { goto cleanup; } @@ -792,7 +653,7 @@ int gs_download_cover(GS_CLIENT hnd, const SERVER_DATA *server, int appid, const return GS_OUT_OF_MEMORY; } - construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, "appasset", + construct_url(hnd, url, sizeof(url), true, server->serverInfo.address, server_port(server, true), "appasset", "appid=%d&AssetType=2&AssetIdx=0", appid); ret = http_request(hnd->http, url, data); if (ret != GS_OK) { @@ -843,32 +704,237 @@ void gs_set_timeout(GS_CLIENT hnd, int timeout_secs) { http_set_timeout(hnd->http, timeout_secs); } -int gs_get_status(GS_CLIENT hnd, PSERVER_DATA server, const char *address, bool unsupported) { +int gs_get_status(GS_CLIENT hnd, PSERVER_DATA server, const char *address, uint16_t port, bool unsupported) { LiInitializeServerInformation(&server->serverInfo); server->serverInfo.address = address; + server->extPort = port; server->unsupported = unsupported; return load_server_status(hnd, server); } -static bool construct_url(GS_CLIENT hnd, char *url, size_t ulen, bool secure, const char *address, const char *action, - const char *fmt, ...) { +static int load_server_status(GS_CLIENT hnd, PSERVER_DATA server) { + int ret = GS_OK; + if (server->extPort != 0 && server->httpsPort == 0) { + ret = resolve_ports(hnd, server->serverInfo.address, server->extPort, &server->httpsPort); + } + if (ret != GS_OK) { + return ret; + } + + char url[4096]; + int i = 0; + do { + char *pairedText = NULL; + char *currentGameText = NULL; + char *stateText = NULL; + char *serverCodecModeSupportText = NULL; + char *httpsPortText = NULL; + char *externalPortText = NULL; + + ret = GS_INVALID; + + // Modern GFE versions don't allow serverinfo to be fetched over HTTPS if the client + // is not already paired. Since we can't pair without knowing the server version, we + // make another request over HTTP if the HTTPS request fails. We can't just use HTTP + // for everything because it doesn't accurately tell us if we're paired. + bool secure = i == 0; + construct_url(hnd, url, sizeof(url), secure, server->serverInfo.address, server_port(server, secure), + "serverinfo", NULL); + + HTTP_DATA *data = http_data_alloc(); + if (data == NULL) { + ret = GS_OUT_OF_MEMORY; + goto cleanup; + } + if (http_request(hnd->http, url, data) != GS_OK) { + ret = GS_IO_ERROR; + goto cleanup; + } + + if (xml_status(data->memory, data->size) == GS_ERROR) { + ret = GS_ERROR; + goto cleanup; + } + + if (xml_search(data->memory, data->size, "uniqueid", (char **) &server->uuid) != GS_OK) { + goto cleanup; + } + if (xml_search(data->memory, data->size, "mac", (char **) &server->mac) != GS_OK) { + goto cleanup; + } + if (xml_search(data->memory, data->size, "hostname", (char **) &server->hostname) != GS_OK) { + server->hostname = strdup(server->serverInfo.address); + } + + if (xml_search(data->memory, data->size, "currentgame", ¤tGameText) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "PairStatus", &pairedText) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "appversion", (char **) &server->serverInfo.serverInfoAppVersion) != + GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "state", &stateText) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "ServerCodecModeSupport", &serverCodecModeSupportText) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "gputype", (char **) &server->gpuType) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "GsVersion", (char **) &server->gsVersion) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "GfeVersion", (char **) &server->serverInfo.serverInfoGfeVersion) != + GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "HttpsPort", &httpsPortText) != GS_OK) { + goto cleanup; + } + + if (xml_search(data->memory, data->size, "ExternalPort", &externalPortText) != GS_OK) { + goto cleanup; + } + + if (xml_modelist(data->memory, data->size, &server->modes) != GS_OK) { + goto cleanup; + } + + // These fields are present on all version of GFE that this client supports + if (!strlen(currentGameText) || !strlen(pairedText) || !strlen(server->serverInfo.serverInfoAppVersion) || + !strlen(stateText)) { + goto cleanup; + } + + int serverCodecModeSupport = serverCodecModeSupportText == NULL ? 0 : + (int) strtol(serverCodecModeSupportText, NULL, 0); + + server->paired = pairedText != NULL && strcmp(pairedText, "1") == 0; + server->currentGame = currentGameText == NULL ? 0 : (int) strtol(currentGameText, NULL, 0); + server->supports4K = serverCodecModeSupport != 0; + server->supportsHdr = serverCodecModeSupport & 0x200; + server->serverMajorVersion = (int) strtol(server->serverInfo.serverInfoAppVersion, NULL, 0); + // Real Nvidia host software (GeForce Experience and RTX Experience) both use the 'Mjolnir' + // codename in the state field and no version of Sunshine does. We can use this to bypass + // some assumptions about Nvidia hardware that don't apply to Sunshine hosts. + server->isGfe = strstr(stateText, "MJOLNIR") != NULL; + server->httpsPort = httpsPortText == NULL ? 0 : (int) strtol(httpsPortText, NULL, 0); + server->extPort = externalPortText == NULL ? 0 : (int) strtol(externalPortText, NULL, 0); + + if (strstr(stateText, "_SERVER_BUSY") == NULL) { + // After GFE 2.8, current game remains set even after streaming + // has ended. We emulate the old behavior by forcing it to zero + // if streaming is not active. + server->currentGame = 0; + } + ret = GS_OK; + + cleanup: + if (data != NULL) { + http_data_free(data); + } + + if (pairedText != NULL) { + free(pairedText); + } + + if (currentGameText != NULL) { + free(currentGameText); + } + + if (stateText != NULL) { + free(stateText); + } + + if (serverCodecModeSupportText != NULL) { + free(serverCodecModeSupportText); + } + + i++; + } while (ret != GS_OK && i < 2); + + if (ret == GS_OK && !server->unsupported) { + if (server->serverMajorVersion > MAX_SUPPORTED_GFE_VERSION) { + ret = gs_set_error(GS_UNSUPPORTED_VERSION, "Ensure you're running the latest version of Moonlight " + "or downgrade GeForce Experience and try again"); + } else if (server->serverMajorVersion < MIN_SUPPORTED_GFE_VERSION) { + ret = gs_set_error(GS_UNSUPPORTED_VERSION, "Moonlight requires a newer version of GeForce Experience. " + "Please upgrade GFE on your PC and try again."); + } + } + + return ret; +} + +static int resolve_ports(GS_CLIENT hnd, const char *address, uint16_t port, uint16_t *https_port) { + int ret = GS_OK; + char url[4096]; + HTTP_DATA *data = http_data_alloc(); + if (data == NULL) { + return GS_OUT_OF_MEMORY; + } + + construct_url(hnd, url, sizeof(url), false, address, port, "serverinfo", NULL); + if ((ret = http_request(hnd->http, url, data)) != GS_OK) { + goto cleanup; + } + + if (xml_status(data->memory, data->size) == GS_ERROR) { + ret = GS_ERROR; + goto cleanup; + } + + char *httpsPortText = NULL; + if (xml_search(data->memory, data->size, "HttpsPort", &httpsPortText) != GS_OK) { + ret = GS_INVALID; + goto cleanup; + } + + *https_port = (uint16_t) strtol(httpsPortText, NULL, 0); + + cleanup: + http_data_free(data); + return ret; +} + +static bool construct_url(GS_CLIENT hnd, char *url, size_t ulen, bool secure, const char *address, uint16_t port, + const char *action, const char *fmt, ...) { uuidstr_t uuid; if (!uuidstr_random(&uuid)) { return false; } - int port = secure ? 47984 : 47989; + if (port == 0) { + port = secure ? 47984 : 47989; + } char *const proto = secure ? "https" : "http"; + size_t w_len = snprintf(url, ulen, "%s://", proto); + bool is_ipv6 = strchr(address, ':') != NULL; + if (is_ipv6) { + w_len += snprintf(url + w_len, ulen - w_len, "[%s]", address); + } else { + w_len += snprintf(url + w_len, ulen - w_len, "%s", address); + } + w_len += snprintf(url + w_len, ulen - w_len, ":%u/%s?uniqueid=%s&uuid=%.*s", port, action, hnd->unique_id, 36, + uuid.data); if (fmt) { char params[4096]; va_list ap; va_start(ap, fmt); vsnprintf(params, 4096, fmt, ap); va_end(ap); - snprintf(url, ulen, "%s://%s:%d/%s?uniqueid=%s&uuid=%.*s&%s", proto, address, port, action, - hnd->unique_id, 36, uuid.data, params); - } else { - snprintf(url, ulen, "%s://%s:%d/%s?uniqueid=%s&uuid=%.*s", proto, address, port, - action, hnd->unique_id, 36, uuid.data); + snprintf(url + w_len, ulen - w_len, "&%s", params); } return true; } @@ -893,4 +959,8 @@ static bool append_param(char *url, size_t ulen, const char *param, const char * p = stpncpy(p, value, vlen); *p = '\0'; return true; -} \ No newline at end of file +} + +static uint16_t server_port(const SERVER_DATA *server, bool secure) { + return secure ? server->httpsPort : server->extPort; +} diff --git a/src/app/backend/pcmanager.h b/src/app/backend/pcmanager.h index 3bed8a2a3..7dc2a3347 100644 --- a/src/app/backend/pcmanager.h +++ b/src/app/backend/pcmanager.h @@ -6,6 +6,7 @@ #include "libgamestream/client.h" #include "uuidstr.h" +#include "sockaddr.h" typedef struct app_t app_t; typedef struct pcmanager_t pcmanager_t; @@ -76,7 +77,7 @@ void pcmanager_unregister_listener(pcmanager_t *manager, const pcmanager_listene * @param userdata * @return */ -bool pcmanager_manual_add(pcmanager_t *manager, const char *address, pcmanager_callback_t callback, void *userdata); +bool pcmanager_manual_add(pcmanager_t *manager, sockaddr_t *address, pcmanager_callback_t callback, void *userdata); /** * @brief Generates a PIN code, and start pairing process. @@ -132,4 +133,4 @@ bool pcmanager_send_wol(pcmanager_t *manager, const uuidstr_t *uuid, pcmanager_c * @param userdata * @return */ -int pcmanager_update_by_ip(worker_context_t *context, const char *ip, bool force); \ No newline at end of file +int pcmanager_update_by_ip(worker_context_t *context, const char *ip, uint16_t port, bool force); \ No newline at end of file diff --git a/src/app/backend/pcmanager/discovery/discovery_libmicrodns.c b/src/app/backend/pcmanager/discovery/discovery_libmicrodns.c index a8695ddb3..591513657 100644 --- a/src/app/backend/pcmanager/discovery/discovery_libmicrodns.c +++ b/src/app/backend/pcmanager/discovery/discovery_libmicrodns.c @@ -3,6 +3,7 @@ #include #include "util/bus.h" #include "backend/pcmanager/worker/worker.h" +#include "sockaddr.h" struct discovery_task_t { pcmanager_t *manager; @@ -89,12 +90,33 @@ static void discovery_callback(discovery_task_t *task, int status, const struct return; } if (task->stop) { return; } + struct sockaddr *addr = sockaddr_new(); for (const struct rr_entry *cur = entries; cur != NULL; cur = cur->next) { - if (cur->type != RR_A) { continue; } - worker_context_t *ctx = worker_context_new(task->manager, NULL, NULL, NULL); - ctx->arg1 = strdup(cur->data.A.addr_str); - pcmanager_worker_queue(task->manager, worker_host_discovered, ctx); + switch (cur->type) { + case RR_A: { + if (addr->sa_family != AF_UNSPEC) { continue; } + sockaddr_set_ip(addr, AF_INET, &cur->data.A.addr); + break; + } + case RR_AAAA: { + if (addr->sa_family != AF_UNSPEC) { continue; } + // Ignore any link-local addresses + if (IN6_IS_ADDR_LINKLOCAL(&cur->data.AAAA.addr)) { continue; } +// sockaddr_set_ip(addr, AF_INET6, &cur->data.AAAA.addr); + break; + } + case RR_SRV: { + sockaddr_set_port(addr, cur->data.SRV.port); + break; + } + } } + if (addr->sa_family == AF_UNSPEC) { + return; + } + worker_context_t *ctx = worker_context_new(task->manager, NULL, NULL, NULL); + ctx->arg1 = addr; + pcmanager_worker_queue(task->manager, worker_host_discovered, ctx); } static void discovery_finalize(void *arg, int result) { diff --git a/src/app/backend/pcmanager/known_hosts.c b/src/app/backend/pcmanager/known_hosts.c index fba4a8fe0..3b770e41a 100644 --- a/src/app/backend/pcmanager/known_hosts.c +++ b/src/app/backend/pcmanager/known_hosts.c @@ -1,3 +1,4 @@ +#include "known_hosts.h" #include "priv.h" #include "pclist.h" #include "app.h" @@ -8,27 +9,7 @@ #include "util/ini_ext.h" #include "util/path.h" #include "app_settings.h" - -typedef struct known_host_t { - uuidstr_t uuid; - char *mac; - char *hostname; - char *address; - bool selected; - appid_list_t *favs; - appid_list_t *hidden; - struct known_host_t *next; -} known_host_t; - -#define LINKEDLIST_IMPL -#define LINKEDLIST_MODIFIER static -#define LINKEDLIST_TYPE known_host_t -#define LINKEDLIST_PREFIX hostlist - -#include "linked_list.h" - -#undef LINKEDLIST_TYPE -#undef LINKEDLIST_PREFIX +#include "sockaddr.h" #define LINKEDLIST_IMPL #define LINKEDLIST_MODIFIER static @@ -43,26 +24,24 @@ typedef struct known_host_t { #undef LINKEDLIST_TYPE #undef LINKEDLIST_PREFIX -static int known_hosts_parse(known_host_t **list, const char *section, const char *name, const char *value); +static int known_hosts_handle(known_host_t **list, const char *section, const char *name, const char *value); -static int hostlist_find_uuid(known_host_t *node, void *v); - -static void hostlist_node_free(known_host_t *node); +static int known_hosts_find_uuid(known_host_t *node, void *v); void pcmanager_load_known_hosts(pcmanager_t *manager) { commons_log_info("PCManager", "Load unknown hosts"); char *conf_file = path_join(manager->app->settings.conf_dir, CONF_NAME_HOSTS); - known_host_t *hosts = NULL; - if (ini_parse(conf_file, (ini_handler) known_hosts_parse, &hosts) != 0) { - goto cleanup; - } + known_host_t *hosts = known_hosts_parse(conf_file); bool selected_set = false; for (known_host_t *cur = hosts; cur; cur = cur->next) { - const char *mac = cur->mac, *hostname = cur->hostname, *address = cur->address; + const char *mac = cur->mac, *hostname = cur->hostname; + const struct sockaddr *address = cur->address; + char address_buf[64] = {0}; + sockaddr_get_ip_str(address, address_buf, sizeof(address_buf)); if (!mac || !hostname || !address) { commons_log_warn("PCManager", "Unknown host entry: mac=%s, hostname=%s, address=%s", mac, hostname, - address); + address_buf); continue; } @@ -70,7 +49,8 @@ void pcmanager_load_known_hosts(pcmanager_t *manager) { server->uuid = uuidstr_tostr(&cur->uuid); server->mac = mac; server->hostname = hostname; - server->serverInfo.address = address; + server->serverInfo.address = strdup(address_buf); + server->extPort = sockaddr_get_port(address); pclist_t *node = pclist_insert_known(manager, &cur->uuid, server); @@ -85,8 +65,7 @@ void pcmanager_load_known_hosts(pcmanager_t *manager) { selected_set = true; } } - cleanup: - hostlist_free(hosts, hostlist_node_free); + known_hosts_free(hosts, known_hosts_node_free); free(conf_file); } @@ -101,12 +80,24 @@ void pcmanager_save_known_hosts(pcmanager_t *manager) { if (!cur->server || !cur->known) { continue; } + char address_buf[64] = {0}; const SERVER_DATA *server = cur->server; ini_write_section(fp, server->uuid); ini_write_string(fp, "mac", server->mac); ini_write_string(fp, "hostname", server->hostname); - ini_write_string(fp, "address", server->serverInfo.address); + + struct sockaddr *address = sockaddr_new(); + if (sockaddr_set_ip_str(address, strchr(server->serverInfo.address, ':') ? AF_INET6 : AF_INET, + server->serverInfo.address) != 0) { + free(address); + continue; + } + sockaddr_set_port(address, server->extPort); + sockaddr_to_string(address, address_buf, sizeof(address_buf)); + ini_write_string(fp, "address", address_buf); + free(address); + if (!selected_set && cur->selected) { ini_write_bool(fp, "selected", true); selected_set = true; @@ -127,20 +118,20 @@ void pcmanager_save_known_hosts(pcmanager_t *manager) { fclose(fp); } -static int known_hosts_parse(known_host_t **list, const char *section, const char *name, const char *value) { +static int known_hosts_handle(known_host_t **list, const char *section, const char *name, const char *value) { if (!section) { return 1; } - known_host_t *host = hostlist_find_by(*list, section, (hostlist_find_fn) hostlist_find_uuid); + known_host_t *host = known_hosts_find_by(*list, section, (known_hosts_find_fn) known_hosts_find_uuid); if (!host) { - host = hostlist_new(); + host = known_hosts_new(); uuidstr_fromstr(&host->uuid, section); - *list = hostlist_append(*list, host); + *list = known_hosts_append(*list, host); } if (INI_NAME_MATCH("mac")) { host->mac = SDL_strdup(value); } else if (INI_NAME_MATCH("hostname")) { host->hostname = SDL_strdup(value); } else if (INI_NAME_MATCH("address")) { - host->address = SDL_strdup(value); + host->address = sockaddr_parse(value); } else if (INI_NAME_MATCH("selected")) { host->selected = INI_IS_TRUE(value); } else if (INI_NAME_MATCH("favorite")) { @@ -155,11 +146,14 @@ static int known_hosts_parse(known_host_t **list, const char *section, const cha return 1; } -static int hostlist_find_uuid(known_host_t *node, void *v) { +static int known_hosts_find_uuid(known_host_t *node, void *v) { return !uuidstr_t_equals_s(&node->uuid, v); } -static void hostlist_node_free(known_host_t *node) { +void known_hosts_node_free(known_host_t *node) { + if (node->address) { + free(node->address); + } if (node->favs) { appid_list_free(node->favs, (appid_list_nodefree_fn) free); } @@ -167,4 +161,12 @@ static void hostlist_node_free(known_host_t *node) { appid_list_free(node->hidden, (appid_list_nodefree_fn) free); } free(node); +} + +known_host_t *known_hosts_parse(const char *conf_file) { + known_host_t *hosts = NULL; + if (ini_parse(conf_file, (ini_handler) known_hosts_handle, &hosts) != 0) { + return NULL; + } + return hosts; } \ No newline at end of file diff --git a/src/app/backend/pcmanager/known_hosts.h b/src/app/backend/pcmanager/known_hosts.h new file mode 100644 index 000000000..7291977bf --- /dev/null +++ b/src/app/backend/pcmanager/known_hosts.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Ningyuan Li . + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include "backend/types.h" +#include "uuidstr.h" +#include "sockaddr.h" + +typedef struct known_host_t { + uuidstr_t uuid; + char *mac; + char *hostname; + struct sockaddr *address; + bool selected; + appid_list_t *favs; + appid_list_t *hidden; + struct known_host_t *next; +} known_host_t; + +void known_hosts_node_free(known_host_t *node); + +known_host_t *known_hosts_parse(const char *conf_file); + +#define LINKEDLIST_IMPL +#define LINKEDLIST_MODIFIER static +#define LINKEDLIST_TYPE known_host_t +#define LINKEDLIST_PREFIX known_hosts + +#include "linked_list.h" + +#undef LINKEDLIST_TYPE +#undef LINKEDLIST_PREFIX \ No newline at end of file diff --git a/src/app/backend/pcmanager/pairing.c b/src/app/backend/pcmanager/pairing.c index 9e5b68a18..1806a2445 100644 --- a/src/app/backend/pcmanager/pairing.c +++ b/src/app/backend/pcmanager/pairing.c @@ -16,7 +16,7 @@ bool pcmanager_pair(pcmanager_t *manager, const uuidstr_t *uuid, char *pin, pcma return false; } SERVER_DATA *server = node->server; - if (server->paired) return false; + if (server->paired) { return false; } if (server->currentGame) { commons_log_info("PCManager", "The server %s is in game", server->hostname); } @@ -28,9 +28,12 @@ bool pcmanager_pair(pcmanager_t *manager, const uuidstr_t *uuid, char *pin, pcma return true; } -bool pcmanager_manual_add(pcmanager_t *manager, const char *address, pcmanager_callback_t callback, void *userdata) { +bool pcmanager_manual_add(pcmanager_t *manager, sockaddr_t *address, pcmanager_callback_t callback, void *userdata) { + if (address == NULL) { + return false; + } worker_context_t *ctx = worker_context_new(manager, NULL, callback, userdata); - ctx->arg1 = strdup(address); + ctx->arg1 = address; pcmanager_worker_queue(manager, worker_add_by_ip, ctx); return true; } diff --git a/src/app/backend/pcmanager/pcmanager_common.c b/src/app/backend/pcmanager/pcmanager_common.c index e1e90563b..7b153f088 100644 --- a/src/app/backend/pcmanager/pcmanager_common.c +++ b/src/app/backend/pcmanager/pcmanager_common.c @@ -22,6 +22,8 @@ PSERVER_DATA serverdata_clone(const SERVER_DATA *src) { server->mac = strdup_nullable(src->mac); server->hostname = strdup_nullable(src->hostname); server->gpuType = strdup_nullable(src->gpuType); + server->extPort = src->extPort; + server->httpsPort = src->httpsPort; server->paired = src->paired; server->supports4K = src->supports4K; server->supportsHdr = src->supportsHdr; diff --git a/src/app/backend/pcmanager/worker/manual_add.c b/src/app/backend/pcmanager/worker/manual_add.c index 2cafa47f8..274491041 100644 --- a/src/app/backend/pcmanager/worker/manual_add.c +++ b/src/app/backend/pcmanager/worker/manual_add.c @@ -1,10 +1,24 @@ #include "worker.h" #include "backend/pcmanager/priv.h" +#include "errors.h" +#include "sockaddr.h" + +static int updated_by_addr(worker_context_t *context, bool force); int worker_add_by_ip(worker_context_t *context) { - return pcmanager_update_by_ip(context, context->arg1, true); + return updated_by_addr(context, true); } int worker_host_discovered(worker_context_t *context) { - return pcmanager_update_by_ip(context, context->arg1, false); -} \ No newline at end of file + return updated_by_addr(context, false); +} + +int updated_by_addr(worker_context_t *context, bool force) { + struct sockaddr *addr = context->arg1; + char ip[64]; + if (sockaddr_get_ip_str(addr, ip, sizeof(ip)) != 0) { + return GS_FAILED; + } + uint16_t port = sockaddr_get_port(addr); + return pcmanager_update_by_ip(context, ip, port, force); +} diff --git a/src/app/backend/pcmanager/worker/update.c b/src/app/backend/pcmanager/worker/update.c index f4d28484d..17658290c 100644 --- a/src/app/backend/pcmanager/worker/update.c +++ b/src/app/backend/pcmanager/worker/update.c @@ -13,10 +13,13 @@ static void notify_querying(pclist_update_context_t *args); int worker_host_update(worker_context_t *context) { const pclist_t *node = pcmanager_node(context->manager, &context->uuid); - return pcmanager_update_by_ip(context, node->server->serverInfo.address, true); + if (node == NULL) { + return GS_FAILED; + } + return pcmanager_update_by_ip(context, node->server->serverInfo.address, node->server->extPort, true); } -int pcmanager_update_by_ip(worker_context_t *context, const char *ip, bool force) { +int pcmanager_update_by_ip(worker_context_t *context, const char *ip, uint16_t port, bool force) { SDL_assert_release(context != NULL); SDL_assert_release(context->manager != NULL); SDL_assert_release(ip != NULL); @@ -44,7 +47,7 @@ int pcmanager_update_by_ip(worker_context_t *context, const char *ip, bool force } GS_CLIENT client = app_gs_client_new(context->app); PSERVER_DATA server = serverdata_new(); - int ret = gs_get_status(client, server, ip_dup, app_configuration->unsupported); + int ret = gs_get_status(client, server, ip_dup, port, app_configuration->unsupported); ip_dup = NULL; gs_destroy(client); if (existing) { diff --git a/src/app/backend/pcmanager/worker/wol.c b/src/app/backend/pcmanager/worker/wol.c index ef256e4f5..8ac44731a 100644 --- a/src/app/backend/pcmanager/worker/wol.c +++ b/src/app/backend/pcmanager/worker/wol.c @@ -22,7 +22,7 @@ int worker_wol(worker_context_t *context) { int ret; while (!SDL_TICKS_PASSED(SDL_GetTicks(), timeout)) { PSERVER_DATA tmpserver = serverdata_new(); - ret = gs_get_status(gs, tmpserver, strdup(server->serverInfo.address), false); + ret = gs_get_status(gs, tmpserver, strdup(server->serverInfo.address), server->extPort, false); serverdata_free(tmpserver); commons_log_debug("WoL", "gs_get_status returned %d, errno=%d", (int) ret, errno); if (ret == 0 || errno == ECONNREFUSED) { diff --git a/src/app/backend/types.h b/src/app/backend/types.h index 9a530fc33..ca6ebf8d2 100644 --- a/src/app/backend/types.h +++ b/src/app/backend/types.h @@ -15,9 +15,9 @@ typedef enum SERVER_STATE_ENUM { /** Server is online but not paired */ SERVER_STATE_NOT_PAIRED = SERVER_STATE_ONLINE | 0x02, /** Can't reach server */ - SERVER_STATE_OFFLINE = 0x30, + SERVER_STATE_OFFLINE = 0x40, /** Server returned error */ - SERVER_STATE_ERROR = 0x40, + SERVER_STATE_ERROR = 0x80, } SERVER_STATE_ENUM; typedef union SERVER_STATE { diff --git a/src/app/stream/session_worker.c b/src/app/stream/session_worker.c index 55761005c..353835295 100644 --- a/src/app/stream/session_worker.c +++ b/src/app/stream/session_worker.c @@ -102,7 +102,7 @@ int session_worker(session_t *session) { .app = app, .manager = pcmanager, }; - pcmanager_update_by_ip(&update_ctx, server->serverInfo.address, true); + pcmanager_update_by_ip(&update_ctx, server->serverInfo.address, server->extPort, true); // Don't always reset status as error state should be kept session_set_state(session, STREAMING_NONE); diff --git a/src/app/ui/launcher/add.dialog.c b/src/app/ui/launcher/add.dialog.c index cc438b584..f4fc70649 100644 --- a/src/app/ui/launcher/add.dialog.c +++ b/src/app/ui/launcher/add.dialog.c @@ -46,6 +46,7 @@ static lv_obj_t *create_dialog(lv_fragment_t *self, lv_obj_t *parent) { lv_obj_set_style_pad_all(content, lv_dpx(8), 0); lv_obj_set_style_pad_gap(content, lv_dpx(8), 0); lv_obj_set_layout(content, LV_LAYOUT_GRID); + lv_obj_clear_flag(content, LV_OBJ_FLAG_SCROLLABLE); static lv_coord_t col_dsc[] = {LV_GRID_FR(1), LV_GRID_CONTENT, 1, LV_GRID_TEMPLATE_LAST}; static const lv_coord_t row_dsc[] = {LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST}; col_dsc[2] = LV_DPX(10); @@ -58,9 +59,9 @@ static lv_obj_t *create_dialog(lv_fragment_t *self, lv_obj_t *parent) { lv_obj_t *ip_input = lv_textarea_create(content); lv_obj_set_grid_cell(ip_input, LV_GRID_ALIGN_STRETCH, 0, 3, LV_GRID_ALIGN_STRETCH, 1, 1); - lv_textarea_set_placeholder_text(ip_input, locstr("IPv4 address only")); + lv_textarea_set_placeholder_text(ip_input, locstr("IPv4 or IPv6 address")); lv_textarea_set_one_line(ip_input, true); - lv_textarea_set_accepted_chars(ip_input, ".-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + lv_textarea_set_accepted_chars(ip_input, ".-_:[]0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); lv_obj_add_event_cb(ip_input, input_changed_cb, LV_EVENT_VALUE_CHANGED, controller); lv_obj_add_event_cb(ip_input, input_key_cb, LV_EVENT_KEY, controller); lv_obj_add_event_cb(ip_input, input_cancel_cb, LV_EVENT_CANCEL, controller); @@ -74,7 +75,7 @@ static lv_obj_t *create_dialog(lv_fragment_t *self, lv_obj_t *parent) { lv_obj_t *add_error = lv_label_create(content); lv_obj_add_flag(add_error, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_height(add_error, LV_SIZE_CONTENT); + lv_obj_set_size(add_error, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_grid_cell(add_error, LV_GRID_ALIGN_START, 0, 3, LV_GRID_ALIGN_STRETCH, 2, 1); lv_label_set_long_mode(add_error, LV_LABEL_LONG_WRAP); lv_label_set_text_static(add_error, locstr("Failed to add computer.")); @@ -93,11 +94,15 @@ static lv_obj_t *create_dialog(lv_fragment_t *self, lv_obj_t *parent) { static void dialog_cb(lv_event_t *event) { add_dialog_controller_t *controller = lv_event_get_user_data(event); lv_obj_t *dialog = lv_event_get_current_target(event); - if (dialog != controller->base.obj) return; + if (dialog != controller->base.obj) { + return; + } uint16_t btn = lv_msgbox_get_active_btn(dialog); if (btn == 1) { - const char *address = lv_textarea_get_text(controller->input); - if (!address || !address[0])return; + sockaddr_t *address = sockaddr_parse(lv_textarea_get_text(controller->input)); + if (!address) { + return; + } lv_obj_add_state(controller->btns, LV_STATE_DISABLED); lv_obj_add_state(controller->input, LV_STATE_DISABLED); lv_obj_clear_flag(controller->progress, LV_OBJ_FLAG_HIDDEN); @@ -110,8 +115,8 @@ static void dialog_cb(lv_event_t *event) { static void input_changed_cb(lv_event_t *event) { add_dialog_controller_t *controller = lv_event_get_user_data(event); - const char *address = lv_textarea_get_text(controller->input); - if (address && address[0]) { + sockaddr_t *address = sockaddr_parse(lv_textarea_get_text(controller->input)); + if (address) { lv_btnmatrix_clear_btn_ctrl(controller->btns, 1, LV_BTNMATRIX_CTRL_DISABLED); } else { lv_btnmatrix_set_btn_ctrl(controller->btns, 1, LV_BTNMATRIX_CTRL_DISABLED); diff --git a/tests/app/CMakeLists.txt b/tests/app/CMakeLists.txt index 146679674..3653952be 100644 --- a/tests/app/CMakeLists.txt +++ b/tests/app/CMakeLists.txt @@ -1,3 +1,4 @@ - add_unit_test(test_app_lifecycle test_app_lifecycle.c) -add_unit_test(test_settings test_settings.c) \ No newline at end of file +add_unit_test(test_settings test_settings.c) + +add_subdirectory(backend) \ No newline at end of file diff --git a/tests/app/backend/CMakeLists.txt b/tests/app/backend/CMakeLists.txt new file mode 100644 index 000000000..836e95b74 --- /dev/null +++ b/tests/app/backend/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(pcmanager) \ No newline at end of file diff --git a/tests/app/backend/pcmanager/CMakeLists.txt b/tests/app/backend/pcmanager/CMakeLists.txt new file mode 100644 index 000000000..bfea330ae --- /dev/null +++ b/tests/app/backend/pcmanager/CMakeLists.txt @@ -0,0 +1 @@ +add_unit_test(test_known_hosts test_known_hosts.c) \ No newline at end of file diff --git a/tests/app/backend/pcmanager/test_known_hosts.c b/tests/app/backend/pcmanager/test_known_hosts.c new file mode 100644 index 000000000..1fa773cf2 --- /dev/null +++ b/tests/app/backend/pcmanager/test_known_hosts.c @@ -0,0 +1,31 @@ +#include "unity.h" +#include "backend/pcmanager/known_hosts.h" + +void setUp(void) { + +} + +void tearDown(void) { + +} + +void test_parse() { + char buf[64]; + known_host_t *hosts = known_hosts_parse(FIXTURES_PATH_PREFIX "hosts_read.ini"); + sockaddr_get_ip_str(hosts->address, buf, sizeof(buf)); + TEST_ASSERT_EQUAL_STRING("192.168.1.100", buf); + TEST_ASSERT_EQUAL(0, sockaddr_get_port(hosts->address)); + + known_host_t *next = hosts->next; + sockaddr_get_ip_str(next->address, buf, sizeof(buf)); + TEST_ASSERT_EQUAL_STRING("192.168.1.101", buf); + TEST_ASSERT_EQUAL(47985, sockaddr_get_port(next->address)); + + known_hosts_free(hosts, known_hosts_node_free); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_parse); + return UNITY_END(); +} \ No newline at end of file diff --git a/tests/fixtures/hosts_read.ini b/tests/fixtures/hosts_read.ini new file mode 100644 index 000000000..4c36fd8c9 --- /dev/null +++ b/tests/fixtures/hosts_read.ini @@ -0,0 +1,12 @@ +[FA084D97-C23A-4DD0-ACBC-837908B00CE6] +mac = aa:bb:cc:dd:ee:ff +hostname = Sunshine +address = 192.168.1.100 +selected = true +; favorites list +favorite = 12345678 + +[43982AE1-2710-409E-9825-964F2C674EB7] +mac = 11:22:33:44:55:66 +hostname = Sunshine +address = 192.168.1.101:47985 \ No newline at end of file diff --git a/third_party/commons b/third_party/commons index dd2e8be76..0a8e5453e 160000 --- a/third_party/commons +++ b/third_party/commons @@ -1 +1 @@ -Subproject commit dd2e8be76a6979df6d821cee11eecab0abfa4f42 +Subproject commit 0a8e5453e7c6119a605f29ccc251cd24925114b2