From 76424950e373d3b04ac3dd13019151bfba3e8423 Mon Sep 17 00:00:00 2001 From: sumuel Date: Mon, 17 Aug 2026 20:44:55 +0000 Subject: Add the files --- src/common/impl/networking_linux.c | 502 +++++++++++++++++++++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 src/common/impl/networking_linux.c (limited to 'src/common/impl/networking_linux.c') diff --git a/src/common/impl/networking_linux.c b/src/common/impl/networking_linux.c new file mode 100644 index 0000000..e253451 --- /dev/null +++ b/src/common/impl/networking_linux.c @@ -0,0 +1,502 @@ +#include "fastfetch.h" +#include "common/networking.h" +#include "common/time.h" +#include "common/library.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" +#include "common/debug.h" + +#include +#include +#include +#include +#include +#include // For FreeBSD +#include +#include +#include + +static const char* tryNonThreadingFastPath(FFNetworkingState* state) { +#if defined(TCP_FASTOPEN) || __APPLE__ + + if (!state->tfo) { + #if __linux__ || __GNU__ + // Linux doesn't support sendto() on unconnected sockets + FF_DEBUG("TCP Fast Open disabled, skipping"); + return "TCP Fast Open disabled"; + #endif + } else { + FF_DEBUG("Attempting to use TCP Fast Open to connect"); + + #ifndef __APPLE__ // On macOS, TCP_FASTOPEN doesn't seem to be needed + // Set TCP Fast Open + int flag = 1; + if (setsockopt(state->sockfd, IPPROTO_TCP, + #ifdef __APPLE__ + // https://github.com/rust-lang/libc/pull/3135 + 0x218 // TCP_FASTOPEN_FORCE_ENABLE + #else + TCP_FASTOPEN + #endif + , + &flag, + sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_FASTOPEN option: %s", strerror(errno)); + return "setsockopt(TCP_FASTOPEN) failed"; + } else { + #if __linux__ || __GNU__ + FF_DEBUG("Successfully set TCP_FASTOPEN option, queue length: %d", flag); + #elif defined(__APPLE__) + FF_DEBUG("Successfully set TCP_FASTOPEN_FORCE_ENABLE option"); + #else + FF_DEBUG("Successfully set TCP_FASTOPEN option"); + #endif + } + #endif + } + + #ifndef __APPLE__ + FF_DEBUG("Using sendto() + MSG_DONTWAIT to send %u bytes of data", state->command.length); + ssize_t sent = sendto(state->sockfd, + state->command.chars, + state->command.length, + #ifdef MSG_FASTOPEN + MSG_FASTOPEN | + #endif + #ifdef MSG_NOSIGNAL + MSG_NOSIGNAL | + #endif + MSG_DONTWAIT, + state->addr->ai_addr, + state->addr->ai_addrlen); + #else + if (fcntl(state->sockfd, F_SETFL, O_NONBLOCK) == -1) { + FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno)); + return "fcntl(F_SETFL) failed"; + } + FF_DEBUG("Using connectx() to send %u bytes of data", state->command.length); + // Use connectx to establish connection and send data in one call + size_t sent; + if (connectx(state->sockfd, + &(sa_endpoints_t) { + .sae_dstaddr = state->addr->ai_addr, + .sae_dstaddrlen = state->addr->ai_addrlen, + }, + SAE_ASSOCID_ANY, + state->tfo ? CONNECT_DATA_IDEMPOTENT : 0, + &(struct iovec) { + .iov_base = state->command.chars, + .iov_len = state->command.length, + }, + 1, + &sent, + NULL) != 0) { + sent = 0; + } + if (fcntl(state->sockfd, F_SETFL, 0) == -1) { + FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno)); + return "fcntl(F_SETFL) failed"; + } + #endif + if (sent > 0 || (errno == EAGAIN || errno == EWOULDBLOCK + #ifdef __APPLE__ + // On macOS EINPROGRESS means the connection cannot be completed immediately + // On Linux, it means the TFO cookie is not available locally + || errno == EINPROGRESS + #endif + )) { + FF_DEBUG( + #ifdef __APPLE__ + "connectx()" + #else + "sendto()" + #endif + " %s (sent=%zd, %s)", + errno == 0 ? "succeeded" : "was in progress", + sent, + strerror(errno)); + freeaddrinfo(state->addr); + state->addr = NULL; + ffStrbufDestroy(&state->command); + return NULL; + } + + FF_DEBUG( + #ifdef __APPLE__ + "connectx()" + #else + "sendto()" + #endif + " failed: %s", + strerror(errno)); + #ifdef __APPLE__ + return "connectx() failed"; + #else + return "sendto() failed"; + #endif +#else + FF_UNUSED(state); + return "TFO support is not available"; +#endif +} + +// Traditional connect and send function +static const char* connectAndSend(FFNetworkingState* state) { + const char* ret = NULL; + FF_DEBUG("Using traditional connection method to connect"); + + FF_DEBUG("Attempting connect() to server..."); + if (connect(state->sockfd, state->addr->ai_addr, state->addr->ai_addrlen) == -1) { + FF_DEBUG("connect() failed: %s", strerror(errno)); + ret = "connect() failed"; + goto error; + } + FF_DEBUG("connect() succeeded"); + + FF_DEBUG("Attempting to send %u bytes of data...", state->command.length); + if (send(state->sockfd, state->command.chars, state->command.length, 0) < 0) { + FF_DEBUG("send() failed: %s", strerror(errno)); + ret = "send() failed"; + goto error; + } + FF_DEBUG("Data sent successfully"); + + goto exit; + +error: + FF_DEBUG("Error occurred, closing socket"); + close(state->sockfd); + state->sockfd = -1; + +exit: + FF_DEBUG("Releasing address info and other resources"); + freeaddrinfo(state->addr); + state->addr = NULL; + ffStrbufDestroy(&state->command); + + return ret; +} + +FF_THREAD_ENTRY_DECL_WRAPPER(connectAndSend, FFNetworkingState*); + +// Parallel DNS resolution and socket creation +static const char* initNetworkingState(FFNetworkingState* state, const char* host, const char* path, const char* headers) { + FF_DEBUG("Initializing network connection state: host=%s, path=%s", host, path); + + // Initialize command and host information + ffStrbufInitA(&state->command, 128); + ffStrbufAppendS(&state->command, "GET "); + ffStrbufAppendS(&state->command, path); + ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: "); + ffStrbufAppendS(&state->command, host); + ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly tell the server we don't need to keep the connection + + // If compression needs to be enabled + if (state->compression) { + FF_DEBUG("Enabling HTTP content compression"); + ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n"); + } + + ffStrbufAppendS(&state->command, headers); + ffStrbufAppendS(&state->command, "\r\n"); + +#ifdef FF_HAVE_THREADS + state->thread = 0; + FF_DEBUG("Thread ID initialized to 0"); +#endif + + const char* ret = NULL; + + struct addrinfo hints = { + .ai_family = state->ipv6 ? AF_INET6 : AF_INET, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_NUMERICSERV + }; + + FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4"); + // Use AI_NUMERICSERV flag to indicate the service is a numeric port, reducing parsing time + + int gaiRes = getaddrinfo(host, "80", &hints, &state->addr); + if (gaiRes != 0) { + FF_DEBUG("getaddrinfo() failed: %s (res=%d)", gai_strerror(gaiRes), gaiRes); + ret = "getaddrinfo() failed"; + goto error; + } + FF_DEBUG("Address resolution successful"); + + FF_DEBUG("Creating socket"); + state->sockfd = socket(state->addr->ai_family, state->addr->ai_socktype, state->addr->ai_protocol); + if (state->sockfd == -1) { + FF_DEBUG("socket() failed: %s", strerror(errno)); + ret = "socket() failed"; + goto error; + } + FF_DEBUG("Socket creation successful: fd=%d", state->sockfd); + + int flag = 1; +#ifdef TCP_NODELAY + // Disable Nagle's algorithm to reduce small packet transmission delay + if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_NODELAY: %s", strerror(errno)); + } else { + FF_DEBUG("Successfully disabled Nagle's algorithm"); + } +#endif + +#ifdef TCP_QUICKACK + // Set TCP_QUICKACK option to avoid delayed acknowledgments + if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_QUICKACK, &flag, sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_QUICKACK: %s", strerror(errno)); + } else { + FF_DEBUG("Successfully enabled TCP quick acknowledgment"); + } +#endif + + if (state->timeout > 0) { + FF_DEBUG("Setting connection timeout: %u ms", state->timeout); + FF_A_UNUSED uint32_t sec = state->timeout / 1000; + if (sec == 0) { + sec = 1; + } + +#ifdef TCP_CONNECTIONTIMEOUT + FF_DEBUG("Using TCP_CONNECTIONTIMEOUT: %u seconds", sec); + setsockopt(state->sockfd, IPPROTO_TCP, TCP_CONNECTIONTIMEOUT, &sec, sizeof(sec)); +#elif defined(TCP_KEEPINIT) + FF_DEBUG("Using TCP_KEEPINIT: %u seconds", sec); + setsockopt(state->sockfd, IPPROTO_TCP, TCP_KEEPINIT, &sec, sizeof(sec)); +#elif defined(TCP_USER_TIMEOUT) + FF_DEBUG("Using TCP_USER_TIMEOUT: %u milliseconds", state->timeout); + setsockopt(state->sockfd, IPPROTO_TCP, TCP_USER_TIMEOUT, &state->timeout, sizeof(state->timeout)); +#else + FF_DEBUG("Current platform does not support TCP connection timeout"); +#endif + } + + return NULL; + +error: + FF_DEBUG("Error occurred during initialization"); + if (state->addr != NULL) { + FF_DEBUG("Releasing address information"); + freeaddrinfo(state->addr); + state->addr = NULL; + } + + if (state->sockfd > 0) { + FF_DEBUG("Closing socket: fd=%d", state->sockfd); + close(state->sockfd); + state->sockfd = -1; + } + return ret; +} + +const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers) { + FF_DEBUG("Preparing to send HTTP request: host=%s, path=%s", host, path); + + if (state->compression) { + FF_DEBUG("Compression enabled, checking if zlib is available"); + +#ifdef FF_HAVE_ZLIB + const char* zlibError = ffNetworkingLoadZlibLibrary(); + // Only enable compression if zlib library is successfully loaded + if (zlibError == NULL) { + FF_DEBUG("Successfully loaded zlib library, compression enabled"); + } else { + FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError); + state->compression = false; + } +#else + FF_DEBUG("zlib not supported at build time, compression disabled"); + state->compression = false; +#endif + } else { + FF_DEBUG("Compression disabled"); + } + + const char* initResult = initNetworkingState(state, host, path, headers); + if (initResult != NULL) { + FF_DEBUG("Initialization failed: %s", initResult); + return initResult; + } + FF_DEBUG("Network state initialization successful"); + + const char* tfoResult = tryNonThreadingFastPath(state); + if (tfoResult == NULL) { + FF_DEBUG("TryNonThreadingFastPath() succeeded or in progress"); + return NULL; + } + FF_DEBUG("TryNonThreadingFastPath() failed: %s, trying traditional connection", tfoResult); + +#ifdef FF_HAVE_THREADS + if (instance.config.general.multithreading) { + FF_DEBUG("Multithreading mode enabled, creating connection thread"); + state->thread = ffThreadCreate(connectAndSendThreadMain, state); + if (state->thread) { + FF_DEBUG("Thread creation successful: thread=%p", (void*) (uintptr_t) state->thread); + return NULL; + } + FF_DEBUG("Thread creation failed"); + } else { + FF_DEBUG("Multithreading mode disabled, connecting in main thread"); + } +#endif + + return connectAndSend(state); +} + +const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) { + assert(buffer->allocated > 0); + FF_DEBUG("Preparing to receive HTTP response"); + uint32_t timeout = state->timeout; + +#ifdef FF_HAVE_THREADS + if (state->thread) { + FF_DEBUG("Connection thread is running, waiting for it to complete (timeout=%u ms)", timeout); + if (!ffThreadJoin(state->thread, timeout)) { + FF_DEBUG("Thread join failed or timed out"); + return "ffThreadJoin() failed or timeout"; + } + FF_DEBUG("Thread completed successfully"); + state->thread = 0; + } +#endif + + if (state->sockfd == -1) { + FF_DEBUG("Invalid socket, HTTP request might have failed"); + return "ffNetworkingSendHttpRequest() failed"; + } + + // Set larger initial receive buffer instead of small repeated receives + int rcvbuf = 65536; // 64KB + setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + +#ifdef __APPLE__ + // poll for the socket to be readable. + // Because of the non-blocking connectx() call, the connection might not be established yet + FF_DEBUG("Using poll() to check if socket is readable"); + { + int pollRes = poll(&(struct pollfd) { + .fd = state->sockfd, + .events = POLLIN }, + 1, + timeout > 0 ? (int) timeout : -1); + if (pollRes == 0) { + FF_DEBUG("poll() timed out after %u ms", timeout); + close(state->sockfd); + state->sockfd = -1; + return "poll() timeout"; + } else if (pollRes == -1) { + FF_DEBUG("poll() failed: %s", strerror(errno)); + close(state->sockfd); + state->sockfd = -1; + return "poll() failed"; + } + } + FF_DEBUG("Socket is readable, proceeding to receive data"); +#else + if (timeout > 0) { + FF_DEBUG("Setting receive timeout: %u ms", timeout); + struct timeval timev; + timev.tv_sec = timeout / 1000; + timev.tv_usec = (__typeof__(timev.tv_usec)) ((timeout % 1000) * 1000); // milliseconds to microseconds + setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, &timev, sizeof(timev)); + } +#endif + + if (shutdown(state->sockfd, SHUT_WR) == -1) { + FF_DEBUG("Failed to shutdown socket send: %s", strerror(errno)); + // Not a critical error, continue anyway + } + + FF_DEBUG("Starting data reception"); + FF_A_UNUSED int recvCount = 0; + uint32_t contentLength = 0; + uint32_t headerEnd = 0; + + do { + FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u", + ++recvCount, + buffer->length, + ffStrbufGetFree(buffer)); + + // We set `Connection: close`, so the server will close the connection when done. + // Thus we can use MSG_WAITALL to wait until the buffer is full or the connection is closed. + ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, ffStrbufGetFree(buffer), MSG_WAITALL); + + if (received <= 0) { + if (received == 0) { + FF_DEBUG("Connection closed (received=0)"); + } else { + FF_DEBUG("Reception failed: %s", strerror(errno)); + } + break; + } + + buffer->length += (uint32_t) received; + buffer->chars[buffer->length] = '\0'; + + FF_DEBUG("Successfully received %zd bytes of data, total: %u bytes", received, buffer->length); + + // Check if HTTP header end marker is found + if (headerEnd == 0) { + char* pHeaderEnd = memmem(buffer->chars, buffer->length, "\r\n\r\n", 4); + if (pHeaderEnd) { + headerEnd = (uint32_t) (pHeaderEnd - buffer->chars); + FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd); + + // Check for Content-Length header to pre-allocate enough memory + const char* clHeader = strcasestr(buffer->chars, "Content-Length:"); + if (clHeader) { + contentLength = (uint32_t) strtoul(clHeader + 15, NULL, 10); + if (contentLength > 0) { + FF_DEBUG("Detected Content-Length: %u, pre-allocating buffer", contentLength); + // Ensure buffer is large enough, adding header size and some margin + ffStrbufEnsureFree(buffer, contentLength + 16); + FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated); + } + } + } + } + } while (ffStrbufGetFree(buffer) > 0); + + FF_DEBUG("Closing socket: fd=%d", state->sockfd); + close(state->sockfd); + state->sockfd = -1; + + if (buffer->length == 0) { + FF_DEBUG("Server response is empty"); + return "Empty server response received"; + } + + if (headerEnd == 0) { + FF_DEBUG("No HTTP header end marker found"); + return "No HTTP header end found"; + } + + if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n")) { + FF_DEBUG("Invalid response: %.40s...", buffer->chars); + return "Invalid response"; + } + FF_DEBUG("Received valid HTTP 200 response, content %u bytes, total %u bytes", contentLength, buffer->length); + + if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) { + FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4); + return "Content length mismatch"; + } + +// If compression was used, try to decompress +#ifdef FF_HAVE_ZLIB + if (state->compression) { + FF_DEBUG("Content received, checking if compressed"); + if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) { + FF_DEBUG("Decompression failed or invalid compression format"); + return "Failed to decompress or invalid format"; + } else { + FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length); + } + } +#endif + + return NULL; +} -- cgit v1.2.3