From 0325a3b88c533157e61f40e3462ea1395847a215 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Fri, 23 Sep 2022 22:48:43 -0500 Subject: [PATCH] Add support for custom ports with Sunshine --- libgamestream/client.c | 221 +++++++++++++++++++++------------------ libgamestream/client.h | 2 +- libgamestream/discover.c | 18 +++- libgamestream/discover.h | 2 +- src/config.c | 7 +- src/config.h | 3 +- src/main.c | 4 +- 7 files changed, 147 insertions(+), 110 deletions(-) diff --git a/libgamestream/client.c b/libgamestream/client.c index 29e62b2..61b0cbf 100644 --- a/libgamestream/client.c +++ b/libgamestream/client.c @@ -166,109 +166,128 @@ static int load_cert(const char* keyDirectory) { return GS_OK; } -static int load_server_status(PSERVER_DATA server) { - +static int load_serverinfo(PSERVER_DATA server, bool https) { uuid_t uuid; char uuid_str[UUID_STRLEN]; - - int ret; char url[4096]; + int ret = GS_INVALID; + char *pairedText = NULL; + char *currentGameText = NULL; + char *stateText = NULL; + char *serverCodecModeSupportText = NULL; + char *httpsPortText = NULL; + + uuid_generate_random(uuid); + uuid_unparse(uuid, uuid_str); + + snprintf(url, sizeof(url), "%s://%s:%d/serverinfo?uniqueid=%s&uuid=%s", + https ? "https" : "http", server->serverInfo.address, https ? server->httpsPort : server->httpPort, unique_id, uuid_str); + + PHTTP_DATA data = http_create_data(); + if (data == NULL) { + ret = GS_OUT_OF_MEMORY; + goto cleanup; + } + if (http_request(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, "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", &server->gpuType) != GS_OK) + goto cleanup; + + if (xml_search(data->memory, data->size, "GsVersion", &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_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; + + server->paired = pairedText != NULL && strcmp(pairedText, "1") == 0; + server->currentGame = currentGameText == NULL ? 0 : atoi(currentGameText); + server->supports4K = serverCodecModeSupportText != NULL; + server->serverMajorVersion = atoi(server->serverInfo.serverInfoAppVersion); + + server->httpsPort = atoi(httpsPortText); + if (!server->httpsPort) + server->httpsPort = 47984; + + 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_free_data(data); + + if (pairedText != NULL) + free(pairedText); + + if (currentGameText != NULL) + free(currentGameText); + + if (serverCodecModeSupportText != NULL) + free(serverCodecModeSupportText); + + if (httpsPortText != NULL) + free(httpsPortText); + + return ret; +} + +static int load_server_status(PSERVER_DATA server) { + int ret; int i; - i = 0; - do { - char *pairedText = NULL; - char *currentGameText = NULL; - char *stateText = NULL; - char *serverCodecModeSupportText = NULL; + /* Fetch the HTTPS port if we don't have one yet */ + if (!server->httpsPort) { + ret = load_serverinfo(server, false); + if (ret != GS_OK) + return ret; + } - ret = GS_INVALID; - - uuid_generate_random(uuid); - uuid_unparse(uuid, uuid_str); - - // 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. - snprintf(url, sizeof(url), "%s://%s:%d/serverinfo?uniqueid=%s&uuid=%s", - i == 0 ? "https" : "http", server->serverInfo.address, i == 0 ? server->httpsPort : server->httpPort, unique_id, uuid_str); - - PHTTP_DATA data = http_create_data(); - if (data == NULL) { - ret = GS_OUT_OF_MEMORY; - goto cleanup; - } - if (http_request(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, "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", &server->gpuType) != GS_OK) - goto cleanup; - - if (xml_search(data->memory, data->size, "GsVersion", &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; - - // 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; - - server->paired = pairedText != NULL && strcmp(pairedText, "1") == 0; - server->currentGame = currentGameText == NULL ? 0 : atoi(currentGameText); - server->supports4K = serverCodecModeSupportText != NULL; - server->serverMajorVersion = atoi(server->serverInfo.serverInfoAppVersion); - - 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_free_data(data); - - if (pairedText != NULL) - free(pairedText); - - if (currentGameText != NULL) - free(currentGameText); - - if (serverCodecModeSupportText != NULL) - free(serverCodecModeSupportText); - - i++; - } while (ret != GS_OK && i < 2); + // 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. + ret = GS_INVALID; + for (i = 0; i < 2 && ret != GS_OK; i++) { + ret = load_serverinfo(server, i == 0); + } if (ret == GS_OK && !server->unsupported) { if (server->serverMajorVersion > MAX_SUPPORTED_GFE_VERSION) { @@ -790,7 +809,9 @@ int gs_quit_app(PSERVER_DATA server) { return ret; } -int gs_init(PSERVER_DATA server, char *address, const char *keyDirectory, int log_level, bool unsupported) { +int gs_init(PSERVER_DATA server, char *address, unsigned short httpPort, const char *keyDirectory, int log_level, bool unsupported) { + char* portSeparator; + mkdirtree(keyDirectory); if (load_unique_id(keyDirectory) != GS_OK) return GS_FAILED; @@ -803,7 +824,7 @@ int gs_init(PSERVER_DATA server, char *address, const char *keyDirectory, int lo LiInitializeServerInformation(&server->serverInfo); server->serverInfo.address = address; server->unsupported = unsupported; - server->httpPort = 47989; - server->httpsPort = 47984; + server->httpPort = httpPort ? httpPort : 47989; + server->httpsPort = 0; /* Populated by load_server_status() */ return load_server_status(server); } diff --git a/libgamestream/client.h b/libgamestream/client.h index 322435a..cc4c41f 100644 --- a/libgamestream/client.h +++ b/libgamestream/client.h @@ -42,7 +42,7 @@ typedef struct _SERVER_DATA { unsigned short httpsPort; } SERVER_DATA, *PSERVER_DATA; -int gs_init(PSERVER_DATA server, char* address, const char *keyDirectory, int logLevel, bool unsupported); +int gs_init(PSERVER_DATA server, char* address, unsigned short httpPort, const char *keyDirectory, int logLevel, bool unsupported); int gs_start_app(PSERVER_DATA server, PSTREAM_CONFIGURATION config, int appId, bool sops, bool localaudio, int gamepad_mask); int gs_applist(PSERVER_DATA server, PAPP_LIST *app_list); int gs_unpair(PSERVER_DATA server); diff --git a/libgamestream/discover.c b/libgamestream/discover.c index b4367e6..28cc052 100644 --- a/libgamestream/discover.c +++ b/libgamestream/discover.c @@ -33,6 +33,11 @@ static AvahiSimplePoll *simple_poll = NULL; +struct cb_ctx { + char* address; + unsigned short* port; +}; + static void client_callback(AvahiClient *c, AvahiClientState state, void *userdata) { if (state == AVAHI_CLIENT_FAILURE) { gs_error = "Server connection failure"; @@ -43,12 +48,14 @@ static void client_callback(AvahiClient *c, AvahiClientState state, void *userda static void resolve_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event, const char *name, const char *type, const char *domain, const char *host_name, const AvahiAddress *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void *userdata) { if (event == AVAHI_RESOLVER_FOUND) { if (userdata != NULL) { - avahi_address_snprint(userdata, AVAHI_ADDRESS_STR_MAX, address); + struct cb_ctx* ctx = userdata; + avahi_address_snprint(ctx->address, AVAHI_ADDRESS_STR_MAX, address); + *ctx->port = port; avahi_simple_poll_quit(simple_poll); } else { char strAddress[AVAHI_ADDRESS_STR_MAX]; avahi_address_snprint(strAddress, sizeof(strAddress), address); - printf(" %s (%s)\n", host_name, strAddress); + printf(" %s (%s:%u)\n", host_name, strAddress, port); } } @@ -73,7 +80,7 @@ static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, Avah } } -void gs_discover_server(char* dest) { +void gs_discover_server(char* dest, unsigned short* port) { AvahiClient *client = NULL; AvahiServiceBrowser *sb = NULL; @@ -89,7 +96,10 @@ void gs_discover_server(char* dest) { goto cleanup; } - if (!(sb = avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET, "_nvstream._tcp", NULL, 0, browse_callback, dest))) { + struct cb_ctx ctx; + ctx.address = dest; + ctx.port = port; + if (!(sb = avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET, "_nvstream._tcp", NULL, 0, browse_callback, &ctx))) { gs_error = "Failed to create service browser"; goto cleanup; } diff --git a/libgamestream/discover.h b/libgamestream/discover.h index d5468da..4bfd600 100644 --- a/libgamestream/discover.h +++ b/libgamestream/discover.h @@ -21,4 +21,4 @@ #define MAX_ADDRESS_SIZE 40 -void gs_discover_server(char* dest); +void gs_discover_server(char* dest, unsigned short* port); diff --git a/src/config.c b/src/config.c index 6054798..8390642 100644 --- a/src/config.c +++ b/src/config.c @@ -73,6 +73,7 @@ static struct option long_options[] = { {"debug", no_argument, NULL, 'Z'}, {"nomouseemulation", no_argument, NULL, '4'}, {"pin", required_argument, NULL, '5'}, + {"port", required_argument, NULL, '6'}, {0, 0, 0, 0}, }; @@ -248,6 +249,9 @@ static void parse_argument(int c, char* value, PCONFIGURATION config) { case '5': config->pin = atoi(value); break; + case '6': + config->port = atoi(value); + break; case 1: if (config->action == NULL) config->action = value; @@ -369,6 +373,7 @@ void config_parse(int argc, char* argv[], PCONFIGURATION config) { config->rotate = 0; config->codec = CODEC_UNSPECIFIED; config->pin = 0; + config->port = 47989; config->inputsCount = 0; config->mapping = get_path("gamecontrollerdb.txt", getenv("XDG_DATA_DIRS")); @@ -386,7 +391,7 @@ void config_parse(int argc, char* argv[], PCONFIGURATION config) { } else { int option_index = 0; int c; - while ((c = getopt_long_only(argc, argv, "-abc:d:efg:h:i:j:k:lm:no:p:q:r:s:tu:v:w:xy45:", long_options, &option_index)) != -1) { + while ((c = getopt_long_only(argc, argv, "-abc:d:efg:h:i:j:k:lm:no:p:q:r:s:tu:v:w:xy45:6:", long_options, &option_index)) != -1) { parse_argument(c, optarg, config); } } diff --git a/src/config.h b/src/config.h index eb49d4b..753c39c 100644 --- a/src/config.h +++ b/src/config.h @@ -47,7 +47,8 @@ typedef struct _CONFIGURATION { char* inputs[MAX_INPUTS]; int inputsCount; enum codecs codec; - int pin; + int pin; + unsigned short port; } CONFIGURATION, *PCONFIGURATION; extern bool inputAdded; diff --git a/src/main.c b/src/main.c index bca9974..71fddac 100644 --- a/src/main.c +++ b/src/main.c @@ -263,7 +263,7 @@ int main(int argc, char* argv[]) { } config.address[0] = 0; printf("Searching for server...\n"); - gs_discover_server(config.address); + gs_discover_server(config.address, &config.port); if (config.address[0] == 0) { fprintf(stderr, "Autodiscovery failed. Specify an IP address next time.\n"); exit(-1); @@ -279,7 +279,7 @@ int main(int argc, char* argv[]) { printf("Connecting to %s...\n", config.address); int ret; - if ((ret = gs_init(&server, config.address, config.key_dir, config.debug_level, config.unsupported)) == GS_OUT_OF_MEMORY) { + if ((ret = gs_init(&server, config.address, config.port, config.key_dir, config.debug_level, config.unsupported)) == GS_OUT_OF_MEMORY) { fprintf(stderr, "Not enough memory\n"); exit(-1); } else if (ret == GS_ERROR) {