diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 90ceeb6be..36140065a 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -13,6 +13,7 @@ add_subdirectory(obs-scripting) add_subdirectory(opts-parser) add_subdirectory(libcaption) add_subdirectory(uthash) +add_subdirectory(happy-eyeballs) # Use bundled jansson version as fallback find_package(Jansson 2.5 QUIET) diff --git a/deps/happy-eyeballs/CMakeLists.txt b/deps/happy-eyeballs/CMakeLists.txt new file mode 100644 index 000000000..cc7a7a1b1 --- /dev/null +++ b/deps/happy-eyeballs/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +add_library(happy-eyeballs INTERFACE) +add_library(OBS::happy-eyeballs ALIAS happy-eyeballs) + +target_sources(happy-eyeballs INTERFACE happy-eyeballs.c happy-eyeballs.h) + +target_include_directories(happy-eyeballs INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(happy-eyeballs INTERFACE OBS::libobs) + +if(OS_WINDOWS) + target_link_libraries(happy-eyeballs INTERFACE OBS::w32-pthreads) +endif() + +set_target_properties(happy-eyeballs PROPERTIES FOLDER deps) diff --git a/deps/happy-eyeballs/happy-eyeballs.c b/deps/happy-eyeballs/happy-eyeballs.c new file mode 100644 index 000000000..0ed15376b --- /dev/null +++ b/deps/happy-eyeballs/happy-eyeballs.c @@ -0,0 +1,764 @@ +/** + * Copyright (c) 2023 Twitch Interactive, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include "happy-eyeballs.h" +#include "util/darray.h" +#include "util/dstr.h" +#include "util/platform.h" +#include "util/threading.h" +#include +#include +#include + +#ifdef _WIN32 +#ifdef _MSC_VER +#pragma warning(disable : 4996) /* depricated warnings */ +#endif +#include +#include +#define GetSockError() WSAGetLastError() +#ifdef _MSC_VER +#define snprintf _snprintf +#endif +#else /* !_WIN32 */ +#include +#include +#include +#define GetSockError() errno +#endif + +/* ------------------------------------------------------------------------- */ +/* happy eyeballs coefficients */ + +/* this is the same default as libcurl */ +#define HAPPY_EYEBALLS_DELAY_MS 200 +#define HAPPY_EYEBALLS_MAX_ATTEMPTS 6 +/* Total time to wait for sockets or to finish trying; whichever is shorter */ +#define HAPPY_EYEBALLS_CONNECTION_TIMEOUT_MS 25000 + +/* ------------------------------------------------------------------------- */ + +#ifndef INVALID_SOCKET +#define INVALID_SOCKET ~0 +#endif + +#define STATUS_SUCCESS 0 +#define STATUS_FAILURE -1 +#define STATUS_INVALID_ARGUMENT -EINVAL + +struct happy_eyeballs_candidate { + SOCKET sockfd; + os_event_t *socket_completed_event; + pthread_t thread; + int error; +}; + +struct happy_eyeballs_ctx { + /** + * socket_fd will be non-zero upon successful connection to the host + * and port specified. + */ + SOCKET socket_fd; + + /** + * winner_addr will be non-zero upon successful connection to the host + * and port specified. + */ + struct sockaddr_storage winner_addr; + + /** + * winner_addr_len will be non-zero upon successful connection to the + * host and port specified. + */ + socklen_t winner_addr_len; + + /** + * error will be non-zero in case of an error during operation. + */ + int error; + + /** + * error_message may be set when there is a non-zero error code. + */ + const char *error_message; + + /** + * Set along with bind_addr to hint which interface to use. + */ + socklen_t bind_addr_len; + + /** + * Set along with bind_addr_len to hint which interface to use. + */ + struct sockaddr_storage bind_addr; + + /** + * List of in-flight connection attempts. + */ + DARRAY(struct happy_eyeballs_candidate) candidates; + + /** + * Protects against multiple simultaneous winners writing socket_fd, + * winner_addr, and winner_addr_len + */ + pthread_mutex_t winner_mutex; + + /** + * Protects against mutating while iterating the candidate list + */ + pthread_mutex_t candidate_mutex; + + /** + * Event that signals completion of the race, either via winner or + * error + */ + os_event_t *race_completed_event; + + /** + * Addresses gathered by getaddrinfo + */ + struct addrinfo *addresses; + + /** + * Domain name resolution time, in nanoseconds (0 until resolution + * success) + */ + uint64_t name_resolution_time_ns; + + /** + * Connection time, in nanoseconds + */ + uint64_t connection_time_start; + + uint64_t connection_time_end; + + /** + * Indicates whether we are in the initial phase of dispatching + * connections, or if we are waiting for things to connect or time out. + */ + volatile bool is_starting; +}; + +struct happy_connect_worker_args { + SOCKET sockfd; + struct happy_eyeballs_ctx *context; + struct happy_eyeballs_candidate *candidate; + struct addrinfo *address; +}; + +static int check_comodo(struct happy_eyeballs_ctx *context) +{ +#ifdef _WIN32 + /* COMODO security software sandbox blocks all DNS by returning "host + * not found" */ + HOSTENT *h = gethostbyname("localhost"); + if (!h && GetLastError() == WSAHOST_NOT_FOUND) { + context->error = WSAHOST_NOT_FOUND; + context->error_message = + "happy-eyeballs: Connection test failed. " + "This error is likely caused by Comodo Internet " + "Security running OBS in sandbox mode. Please add " + "OBS to the Comodo automatic sandbox exclusion list, " + "restart OBS and try again (11001)."; + return STATUS_FAILURE; + } +#else + (void)context; +#endif + return STATUS_SUCCESS; +} + +static int build_addr_list(const char *hostname, int port, + struct happy_eyeballs_ctx *context) +{ + struct addrinfo hints = {0}; + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + if (context->bind_addr_len == sizeof(struct sockaddr_in)) + hints.ai_family = AF_INET; + else if (context->bind_addr_len == sizeof(struct sockaddr_in6)) + hints.ai_family = AF_INET6; + + struct dstr port_str; + dstr_init(&port_str); + dstr_printf(&port_str, "%d", port); + + uint64_t start_time = os_gettime_ns(); + int err = getaddrinfo(hostname, port_str.array, &hints, + &context->addresses); + context->name_resolution_time_ns = os_gettime_ns() - start_time; + dstr_free(&port_str); + if (err) { + context->error = GetSockError(); + context->error_message = strerror(GetSockError()); + return STATUS_FAILURE; + } + + /* Reorder addresses interleaving address family */ + struct addrinfo *prev = context->addresses; + struct addrinfo *cur = prev->ai_next; + + while (cur) { + if (prev->ai_family == cur->ai_family && + (cur->ai_family == AF_INET || cur->ai_family == AF_INET6)) { + /* If the current protocol family matches the previous + * one, look for the next instance of the other kind */ + const int target_family = + prev->ai_family == AF_INET ? AF_INET6 : AF_INET; + struct addrinfo *it = cur->ai_next; + struct addrinfo *prev_it = cur; + + while (it) { + if (it->ai_family == target_family) + break; + prev_it = it; + it = it->ai_next; + } + + if (!it) { + /* we're at the end and haven't found the other + * kind, exit the loop early. */ + break; + } + + prev->ai_next = it; + prev_it->ai_next = it->ai_next; + it->ai_next = cur; + } + prev = cur; + cur = cur->ai_next; + } + + return STATUS_SUCCESS; +} + +static void signal_end(struct happy_eyeballs_ctx *context) +{ + if (os_event_try(context->race_completed_event) != EAGAIN) + return; + + context->connection_time_end = os_gettime_ns(); + os_event_signal(context->race_completed_event); +} + +/** + * Takes the errors that may have been generated by workers, finds the most + * common one, and sets that to be the overall context error. + */ +static int coalesce_errors(struct happy_eyeballs_ctx *context) +{ + /* Don't coalesce errors while starting */ + if (os_atomic_load_bool(&context->is_starting)) + return STATUS_FAILURE; + + /* Don't coalesce errors if we've already completed */ + if (os_event_try(context->race_completed_event) != EAGAIN) + return STATUS_FAILURE; + + /* We'll use the mode of the errors for now. */ + struct mode { + int error; + int count; + }; + DARRAY(struct mode) modes = {0}; + da_init(modes); + + /* Gather all the errors into counts for each error */ + pthread_mutex_lock(&context->candidate_mutex); + for (size_t i = 0; i < context->candidates.num; i++) { + int err = context->candidates.array[i].error; + struct mode *mode = NULL; + + /* If the error is 0, just move on. */ + if (err == 0) { + continue; + } + + /* Find an existing index containing this error if it exists */ + for (size_t j = 0; j < modes.num && mode == NULL; j++) { + if (modes.array[j].error == err) + mode = &modes.array[j]; + } + + /* Existing index doesn't exist, take the next available. */ + if (mode == NULL) { + mode = da_push_back_new(modes); + } + + /* Note the error code and increment the count. */ + mode->error = err; + mode->count++; + } + pthread_mutex_unlock(&context->candidate_mutex); + + int max_count = 0; + int max_value = 0; + + /* Find the error with the most occurrences. */ + for (size_t i = 0; i < modes.num; i++) { + if (max_count < modes.array[i].count) { + max_value = modes.array[i].error; + max_count = modes.array[i].count; + } + } + da_free(modes); + + /* Set the error */ + context->error = max_value; + context->error_message = strerror(context->error); + + return STATUS_SUCCESS; +} + +static void *happy_connect_worker(void *arg) +{ + struct happy_connect_worker_args *args = + (struct happy_connect_worker_args *)arg; + struct happy_eyeballs_ctx *context = args->context; + + if (args->sockfd == INVALID_SOCKET) { + goto success; + } + if (os_event_try(args->context->race_completed_event) == 0) { + /* Already lost, don't bother */ + goto success; + } + +#if !defined(_WIN32) && defined(SO_NOSIGPIPE) + setsockopt(args->sockfd, SOL_SOCKET, SO_NOSIGPIPE, &(int){1}, + sizeof(int)); +#endif + if (context->bind_addr.ss_family != 0 && + bind(args->sockfd, (const struct sockaddr *)&context->bind_addr, + context->bind_addr_len) < 0) { + goto failure; + } + + if (connect(args->sockfd, args->address->ai_addr, + (int)args->address->ai_addrlen) == 0) { + + /* success, check if we're the winner. */ + pthread_mutex_lock(&context->winner_mutex); + os_event_signal(args->candidate->socket_completed_event); + + if (os_event_try(context->race_completed_event) == EAGAIN) { + /* We are the winner. */ + context->socket_fd = args->sockfd; + memcpy(&context->winner_addr, args->address->ai_addr, + args->address->ai_addrlen); + context->winner_addr_len = + (socklen_t)args->address->ai_addrlen; + signal_end(context); + } + + pthread_mutex_unlock(&context->winner_mutex); + goto success; + } + +failure: + /* failure, note down the error */ + args->candidate->error = GetSockError(); + pthread_mutex_lock(&context->winner_mutex); + os_event_signal(args->candidate->socket_completed_event); + + /* connection candidates may still be getting dispatched, treat as if + * there's an active candidate */ + bool active = os_atomic_load_bool(&context->is_starting); + + /* check if we are the last worker running. If so, we'll set the error + * status and signal the completion event. */ + pthread_mutex_lock(&context->candidate_mutex); + for (size_t i = 0; i < context->candidates.num && !active; i++) { + active = os_event_try(context->candidates.array[i] + .socket_completed_event) == + EAGAIN; + } + + pthread_mutex_unlock(&context->candidate_mutex); + pthread_mutex_unlock(&context->winner_mutex); + + if (active || context->error != 0) { + /* we're not last or there is already an error on the context, + * let's exit. */ + goto success; + } + + /* Ok, we are last. Coalesce errors and signal completion. */ + if (coalesce_errors(context) == STATUS_SUCCESS) + signal_end(context); + +success: + free(args); + return NULL; +} + +static int launch_worker(struct happy_eyeballs_ctx *context, + struct addrinfo *addr) +{ +#ifdef _WIN32 + SOCKET fd = WSASocket(addr->ai_family, SOCK_STREAM, IPPROTO_TCP, NULL, + 0, WSA_FLAG_OVERLAPPED); +#else + SOCKET fd = socket(addr->ai_family, SOCK_STREAM, IPPROTO_TCP); +#endif + if (fd == INVALID_SOCKET) { + context->error = GetSockError(); + return STATUS_FAILURE; + } + + pthread_mutex_lock(&context->candidate_mutex); + + struct happy_eyeballs_candidate *candidate = + da_push_back_new(context->candidates); + candidate->sockfd = fd; + struct happy_connect_worker_args *args = + (struct happy_connect_worker_args *)malloc( + sizeof(struct happy_connect_worker_args)); + if (args == NULL) { + context->error = ENOMEM; + context->error_message = "happy-eyeballs: Failed to allocate " + "memory for worker context"; + pthread_mutex_unlock(&context->candidate_mutex); + return STATUS_FAILURE; + } + + int result = os_event_init(&candidate->socket_completed_event, + OS_EVENT_TYPE_MANUAL); + if (result != 0) { + /* failure to create the socket completed event */ + context->error = result; + context->error_message = "happy-eyeballs: Failed to initialize " + "socket completed event"; + free(args); + pthread_mutex_unlock(&context->candidate_mutex); + return STATUS_FAILURE; + } + + args->sockfd = fd; + args->address = addr; + args->context = context; + args->candidate = candidate; + pthread_mutex_unlock(&context->candidate_mutex); + + /* Launch worker thread; `args` ownership is passed to this thread */ + result = pthread_create(&candidate->thread, NULL, happy_connect_worker, + args); + if (result != 0) { + /* failure to start the worker thread */ + context->error = result; + context->error_message = strerror(result); + os_event_destroy(candidate->socket_completed_event); + free(args); + pthread_mutex_lock(&context->candidate_mutex); + da_erase_item(context->candidates, candidate); + pthread_mutex_unlock(&context->candidate_mutex); + return STATUS_FAILURE; + } + + return STATUS_SUCCESS; +} + +/* ------------------------------------------------------------------------- */ +/* Public functions */ + +int happy_eyeballs_create(struct happy_eyeballs_ctx **context) +{ + if (context == NULL) + return STATUS_INVALID_ARGUMENT; + + struct happy_eyeballs_ctx *ctx = (struct happy_eyeballs_ctx *)malloc( + sizeof(struct happy_eyeballs_ctx)); + + if (ctx == NULL) + return -ENOMEM; + + memset(ctx, 0, sizeof(struct happy_eyeballs_ctx)); + + ctx->socket_fd = INVALID_SOCKET; + da_init(ctx->candidates); + + /* race_completed_event will be signalled when there is a winner or all + * attempts have failed */ + int result = + os_event_init(&ctx->race_completed_event, OS_EVENT_TYPE_MANUAL); + + /* this mutex is used to avoid the situation where we may have two + * simultaneous winners and inconsistent values set to the context. */ + if (result == 0) + result = pthread_mutex_init(&ctx->winner_mutex, NULL); + bool have_winner_mutex = result == 0; + + /* this mutex is used to avoid the situation where we may be mutating + * the candidate array while iterating it. */ + if (result == 0) + result = pthread_mutex_init(&ctx->candidate_mutex, NULL); + bool have_candidate_mutex = result == 0; + + if (result == 0) { + *context = ctx; + return STATUS_SUCCESS; + } + + /* Failure, cleanup */ + if (ctx->race_completed_event) + os_event_destroy((*context)->race_completed_event); + + if (have_winner_mutex) + pthread_mutex_destroy(&(*context)->winner_mutex); + + if (have_candidate_mutex) + pthread_mutex_destroy(&(*context)->candidate_mutex); + + free(ctx); + *context = NULL; + + /* Error codes from pthread_mutex_init are positive, os_event_init is + * negative. We have promised to return negative error codes, so we are + * making them all negative here. */ + return -abs(result); +} + +int happy_eyeballs_connect(struct happy_eyeballs_ctx *context, + const char *hostname, int port) +{ + if (hostname == NULL || context == NULL || port == 0) + return STATUS_INVALID_ARGUMENT; + + int result = check_comodo(context); + if (result == STATUS_SUCCESS) + result = build_addr_list(hostname, port, context); + if (result != STATUS_SUCCESS) + return result; + + context->connection_time_start = os_gettime_ns(); + int prev_family = 0; + struct addrinfo *next = context->addresses; + os_atomic_store_bool(&context->is_starting, true); + + /* Exit the loop under the following conditions: + * 1. We have reached the maximum allowed number of attempts + * 2. There are no more candidates in the linked list (ie. `next` is + * null) + * 3. We have seen two candidates of the same family in a row, stop + * happy eyeballs and let the previous attempt go it alone. */ + for (int i = 0; i < HAPPY_EYEBALLS_MAX_ATTEMPTS && next && + next->ai_family != prev_family; + i++) { + /* Launch a worker thread for this address */ + int result = launch_worker(context, next); + if (result != STATUS_SUCCESS) + return result; + + /* Wait until the delay between attempts times out or we get + * signalled... */ + result = os_event_timedwait(context->race_completed_event, + HAPPY_EYEBALLS_DELAY_MS); + if (result == 0) { + /* signalled. Break out of the loop. */ + break; + } else if (result == EINVAL) { + /* we ran into an error. */ + context->error = result; + context->error_message = "happy-eyeballs: Encountered " + "error waiting for " + "race_completed_event"; + return STATUS_FAILURE; + } + + /* timer timed out, move to the next candidate... */ + prev_family = next->ai_family; + next = next->ai_next; + } + + os_atomic_store_bool(&context->is_starting, false); + + if (happy_eyeballs_try(context) == EAGAIN) { + int active_count = 0; + for (size_t i = 0; i < context->candidates.num; i++) + active_count += + (os_event_try( + context->candidates.array[i] + .socket_completed_event) == + EAGAIN); + if (active_count == 0 && + coalesce_errors(context) == STATUS_SUCCESS) + signal_end(context); + } + + return happy_eyeballs_try(context); +} + +int happy_eyeballs_try(struct happy_eyeballs_ctx *context) +{ + int status = os_event_try(context->race_completed_event); + + if (context->error != 0) + return STATUS_FAILURE; + + if (status != 0 && status != EAGAIN) { + context->error = status; + context->error_message = strerror(status); + return STATUS_FAILURE; + } + return status; +} + +int happy_eyeballs_timedwait_default(struct happy_eyeballs_ctx *context) +{ + return happy_eyeballs_timedwait(context, + HAPPY_EYEBALLS_CONNECTION_TIMEOUT_MS); +} + +int happy_eyeballs_timedwait(struct happy_eyeballs_ctx *context, + unsigned long time_in_millis) +{ + if (context == NULL) + return STATUS_INVALID_ARGUMENT; + + int status = os_event_timedwait(context->race_completed_event, + time_in_millis); + + if (context->error != 0) + return STATUS_FAILURE; + + if (status != 0 && status != ETIMEDOUT) { + context->error = status; + return STATUS_FAILURE; + } + return status; +} + +int happy_eyeballs_destroy(struct happy_eyeballs_ctx *context) +{ + if (context == NULL) + return STATUS_INVALID_ARGUMENT; + +#ifdef _WIN32 +#define SHUT_RDWR SD_BOTH +#else +#define closesocket(s) close(s) +#endif + /* We do not need to lock context->candidate_mutex here because the + * candidate array is _only_ mutated in happy_eyeballs_connect. Using + * this function at the same time as connect is a programmer error. + * Locking it here will cause a deadlock with join. + * + * Shutdown non-winning sockets (but keep the socket alive so that + * things error out in threads) */ + for (size_t i = 0; i < context->candidates.num; i++) { + if (context->candidates.array[i].sockfd != INVALID_SOCKET && + context->candidates.array[i].sockfd != context->socket_fd) { + shutdown(context->candidates.array[i].sockfd, + SHUT_RDWR); + } + } + + /* Join threads */ + for (size_t i = 0; i < context->candidates.num; i++) { + pthread_join(context->candidates.array[i].thread, NULL); + os_event_destroy( + context->candidates.array[i].socket_completed_event); + } + + /* Close sockets */ + for (size_t i = 0; i < context->candidates.num; i++) { + if (context->candidates.array[i].sockfd != INVALID_SOCKET && + context->candidates.array[i].sockfd != context->socket_fd) { + closesocket(context->candidates.array[i].sockfd); + } + } + + pthread_mutex_destroy(&context->winner_mutex); + pthread_mutex_destroy(&context->candidate_mutex); + os_event_destroy(context->race_completed_event); + if (context->addresses != NULL) + freeaddrinfo(context->addresses); + + da_free(context->candidates); + free(context); + return STATUS_SUCCESS; +} + +/* ------------------------------------------------------------------------- */ +/* Setters & Getters */ + +int happy_eyeballs_set_bind_addr(struct happy_eyeballs_ctx *context, + socklen_t addr_len, + struct sockaddr_storage *addr_storage) +{ + if (!context) + return STATUS_INVALID_ARGUMENT; + + if (addr_storage && addr_len > 0) { + memcpy(&context->bind_addr, addr_storage, + sizeof(struct sockaddr_storage)); + context->bind_addr_len = addr_len; + } else { + context->bind_addr_len = 0; + memset(&context->bind_addr, 0, sizeof(struct sockaddr_storage)); + } + return STATUS_SUCCESS; +} + +SOCKET happy_eyeballs_get_socket_fd(const struct happy_eyeballs_ctx *context) +{ + return context ? context->socket_fd : STATUS_INVALID_ARGUMENT; +} + +int happy_eyeballs_get_remote_addr(const struct happy_eyeballs_ctx *context, + struct sockaddr_storage *addr) +{ + if (!context || !addr) + return STATUS_INVALID_ARGUMENT; + if (context->winner_addr_len > 0) + memcpy(addr, &context->winner_addr, context->winner_addr_len); + return context->winner_addr_len; +} + +int happy_eyeballs_get_error_code(const struct happy_eyeballs_ctx *context) +{ + return context ? context->error : STATUS_INVALID_ARGUMENT; +} + +const char * +happy_eyeballs_get_error_message(const struct happy_eyeballs_ctx *context) +{ + return context ? context->error_message : NULL; +} + +uint64_t happy_eyeballs_get_name_resolution_time_ns( + const struct happy_eyeballs_ctx *context) +{ + return context ? context->name_resolution_time_ns : 0; +} + +uint64_t +happy_eyeballs_get_connection_time_ns(const struct happy_eyeballs_ctx *context) +{ + if (!context || + context->connection_time_start > context->connection_time_end) + return 0; + + return context->connection_time_end - context->connection_time_start; +} diff --git a/deps/happy-eyeballs/happy-eyeballs.h b/deps/happy-eyeballs/happy-eyeballs.h new file mode 100644 index 000000000..3ef62e2de --- /dev/null +++ b/deps/happy-eyeballs/happy-eyeballs.h @@ -0,0 +1,191 @@ +/** + * An implementation of RFC 6555 (Happy Eyeballs) to connect to IPv6 hosts with + * IPv4 fast fallback. This implementation currently only works for TCP. + * + * More precisely this algorithm will attempt the configured preferred protocol + * family, followed by the other. So if the user has configured their operating + * system to prefer IPv4 it will attempt IPv4 first, followed by IPv6. + * Generally, though, the default is to prefer IPv6. + * + * Initial implementation by james hurley + */ + +/** + * Copyright (c) 2023 Twitch Interactive, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#define SOCKET int +#endif +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct happy_eyeballs_ctx; + +/** + * Create and initialize a Happy Eyeballs session. This context is expected to + * be accessed and mutated on a single thread. Accessing and mutating this + * context from multiple threads may lead to undefined behavior and deadlocks. + * + * On failure *context will always be NULL. + * + * You must call happy_eyeballs_destroy() to free any resources. + * This function will return 0 on success and *context will be non-NULL. + * + * -EINVAL if context is NULL + * -ENOMEM if there is an allocation failure. + * Another negative error code if there is a failure creating required + * resources. + */ +int happy_eyeballs_create(struct happy_eyeballs_ctx **context); + +/** + * Connect to a provided hostname and port. This function will block for up to + * 1.2 seconds. After this function returns with a success result (0), you need + * to check happy_eyeballs_try or happy_eyeballs_timedwait to determine if a + * connection has been established. + * + * Upon successfully starting the connection process and waiting for the + * initial delay timers, this function will return 0 if the connection has been + * successfully established, or EAGAIN if the caller should wait longer. + * + * This function will return -EINVAL if context, hostname, or port are not set. + */ +int happy_eyeballs_connect(struct happy_eyeballs_ctx *context, + const char *hostname, int port); + +/** + * Optionally set the interface address. You may pass 0 and NULL for these + * parameters, respectively, to clear this setting. This must be called before + * happy_eyeballs_connect. + * + * Returns 0 on success or -EINVAL if context is not set. + */ +int happy_eyeballs_set_bind_addr(struct happy_eyeballs_ctx *context, + socklen_t addr_len, + struct sockaddr_storage *addr_storage); + +/** + * Returns a positive socket file descriptor if a winner is available. + * + * This function will only return a valid socket after happy_eyeballs_try or + * happy_eyeballs_timedwait has returned 0. + * + * Returns -1 if there is no socket available yet or -EINVAL if context is not + * set. + */ +SOCKET happy_eyeballs_get_socket_fd(const struct happy_eyeballs_ctx *context); + +/** + * Fills the addr struct with the winning socket address, if there is one. + * + * This function will only return a valid address after happy_eyeballs_try or + * happy_eyeballs_timedwait has returned 0. + * + * On success, returns the address length of the sockaddr or -EINVAL if context + * is not set. + */ +int happy_eyeballs_get_remote_addr(const struct happy_eyeballs_ctx *context, + struct sockaddr_storage *addr); + +/** + * Returns the current error code for the context or -EINVAL if context is not + * set. + */ +int happy_eyeballs_get_error_code(const struct happy_eyeballs_ctx *context); + +/** + * Returns the current error message for the context. The returned value may be + * NULL if there is no error message. + */ +const char * +happy_eyeballs_get_error_message(const struct happy_eyeballs_ctx *context); + +/** + * Returns the amount of time domain name resolution took, in nanoseconds + */ +uint64_t happy_eyeballs_get_name_resolution_time_ns( + const struct happy_eyeballs_ctx *context); + +/** + * Returns the amount of time connection (or failure) took, in nanoseconds, or + * 0 if happy eyeballs has not yet completed. + */ +uint64_t +happy_eyeballs_get_connection_time_ns(const struct happy_eyeballs_ctx *context); + +/** + * Test if the process has completed without blocking. This function will + * return the following values: + * + * 0 on successful completion. You may use context.socket_fd. + * + * EAGAIN if the process is ongoing. + * + * -1 if some other error has occurred. Check happy_eyeballs_get_error_code and + * happy_eyeballs_get_error_message. + */ +int happy_eyeballs_try(struct happy_eyeballs_ctx *context); + +/** + * Block until the process has completed. + * + * This function will return the following values: + * + * 0 on successful completion. You may use context.socket_fd + * + * -1 on failure. Check happy_eyeballs_get_error_code and + * happy_eyeballs_get_error_message for more details. + * + * ETIMEDOUT on timeout. It is unlikely that context.socket_fd is set. You may + * continue to wait by calling this function again, or call + * happy_eyeballs_destroy to close the session. + */ +int happy_eyeballs_timedwait(struct happy_eyeballs_ctx *context, + unsigned long time_in_millis); + +/** + * Call timedwait with a default wait duration of 25 seconds. + */ +int happy_eyeballs_timedwait_default(struct happy_eyeballs_ctx *context); + +/** + * Releases any resources claimed. Accessing context after calling this + * function will be a use-after-free error. + * + * This function will not close the socket specified in context->socket_fd. You + * must close that yourself! + */ +int happy_eyeballs_destroy(struct happy_eyeballs_ctx *context); + +#ifdef __cplusplus +} +#endif