master ohmu/fastfetch-openbsd / src / common / impl / networking_linux.c
ohmu  ·  2026-10-15
  1#include "fastfetch.h"
  2#include "common/networking.h"
  3#include "common/time.h"
  4#include "common/library.h"
  5#include "common/strutil.h"
  6#include "common/mallocHelper.h"
  7#include "common/debug.h"
  8
  9#include <unistd.h>
 10#include <poll.h>
 11#include <sys/time.h>
 12#include <sys/socket.h>
 13#include <netdb.h>
 14#include <netinet/in.h> // For FreeBSD
 15#include <netinet/tcp.h>
 16#include <errno.h>
 17#include <fcntl.h>
 18
 19// Upper bound of a single HTTP response, guarding against excessive memory allocation
 20#define FF_NETWORKING_MAX_RESPONSE_SIZE (1024u * 1024u)
 21
 22static const char* tryNonThreadingFastPath(FFNetworkingState* state) {
 23#if defined(TCP_FASTOPEN) || __APPLE__
 24
 25    if (!state->tfo) {
 26    #if __linux__ || __GNU__
 27        // Linux doesn't support sendto() on unconnected sockets
 28        FF_DEBUG("TCP Fast Open disabled, skipping");
 29        return "TCP Fast Open disabled";
 30    #endif
 31    } else {
 32        FF_DEBUG("Attempting to use TCP Fast Open to connect");
 33
 34    #ifndef __APPLE__ // On macOS, TCP_FASTOPEN doesn't seem to be needed
 35        // Set TCP Fast Open
 36        int flag = 1;
 37        if (setsockopt(state->sockfd, IPPROTO_TCP,
 38        #ifdef __APPLE__
 39                // https://github.com/rust-lang/libc/pull/3135
 40                0x218 // TCP_FASTOPEN_FORCE_ENABLE
 41        #else
 42                TCP_FASTOPEN
 43        #endif
 44                ,
 45                &flag,
 46                sizeof(flag)) != 0) {
 47            FF_DEBUG("Failed to set TCP_FASTOPEN option: %s", strerror(errno));
 48            return "setsockopt(TCP_FASTOPEN) failed";
 49        } else {
 50        #if __linux__ || __GNU__
 51            FF_DEBUG("Successfully set TCP_FASTOPEN option, queue length: %d", flag);
 52        #elif defined(__APPLE__)
 53            FF_DEBUG("Successfully set TCP_FASTOPEN_FORCE_ENABLE option");
 54        #else
 55            FF_DEBUG("Successfully set TCP_FASTOPEN option");
 56        #endif
 57        }
 58    #endif
 59    }
 60
 61    #ifndef __APPLE__
 62    FF_DEBUG("Using sendto() "
 63        #ifdef MSG_FASTOPEN
 64            "+ MSG_FASTOPEN "
 65        #endif
 66        #ifdef MSG_NOSIGNAL
 67            "+ MSG_NOSIGNAL "
 68        #endif
 69        "to send %u bytes of data", state->command.length);
 70    ssize_t sent = sendto(state->sockfd,
 71        state->command.chars,
 72        state->command.length,
 73        #ifdef MSG_FASTOPEN
 74        MSG_FASTOPEN |
 75        #endif
 76        #ifdef MSG_NOSIGNAL
 77            MSG_NOSIGNAL |
 78        #endif
 79            0,
 80        state->addr->ai_addr,
 81        state->addr->ai_addrlen);
 82    #else
 83    if (fcntl(state->sockfd, F_SETFL, O_NONBLOCK) == -1) {
 84        FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno));
 85        return "fcntl(F_SETFL) failed";
 86    }
 87    FF_DEBUG("Using connectx() to send %u bytes of data", state->command.length);
 88    // Use connectx to establish connection and send data in one call
 89    size_t sent;
 90    if (connectx(state->sockfd,
 91            &(sa_endpoints_t) {
 92                .sae_dstaddr = state->addr->ai_addr,
 93                .sae_dstaddrlen = state->addr->ai_addrlen,
 94            },
 95            SAE_ASSOCID_ANY,
 96            state->tfo ? CONNECT_DATA_IDEMPOTENT : 0,
 97            &(struct iovec) {
 98                .iov_base = state->command.chars,
 99                .iov_len = state->command.length,
100            },
101            1,
102            &sent,
103            nullptr) != 0) {
104        sent = 0;
105    }
106    if (fcntl(state->sockfd, F_SETFL, 0) == -1) {
107        FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno));
108        return "fcntl(F_SETFL) failed";
109    }
110    #endif
111    if (sent > 0 || (errno == EAGAIN || errno == EWOULDBLOCK
112    #ifdef __APPLE__
113                        // On macOS EINPROGRESS means the connection cannot be completed immediately
114                        // On Linux, it means the TFO cookie is not available locally
115                        || errno == EINPROGRESS
116    #endif
117                        )) {
118        FF_DEBUG(
119    #ifdef __APPLE__
120            "connectx()"
121    #else
122            "sendto()"
123    #endif
124            " %s (sent=%zd, %s)",
125            errno == 0 ? "succeeded" : "was in progress",
126            sent,
127            strerror(errno));
128        freeaddrinfo(state->addr);
129        state->addr = nullptr;
130        ffStrbufDestroy(&state->command);
131        return nullptr;
132    }
133
134    FF_DEBUG(
135    #ifdef __APPLE__
136        "connectx()"
137    #else
138        "sendto()"
139    #endif
140        " failed: %s",
141        strerror(errno));
142    #ifdef __APPLE__
143    return "connectx() failed";
144    #else
145    return "sendto() failed";
146    #endif
147#else
148    FF_UNUSED(state);
149    FF_DEBUG("TFO support is not available");
150    return "TFO support is not available";
151#endif
152}
153
154// Traditional connect and send function
155static const char* connectAndSend(FFNetworkingState* state) {
156    const char* ret = nullptr;
157    FF_DEBUG("Using traditional connection method to connect");
158
159    FF_DEBUG("Attempting connect() to server...");
160    if (connect(state->sockfd, state->addr->ai_addr, state->addr->ai_addrlen) == -1) {
161        FF_DEBUG("connect() failed: %s", strerror(errno));
162        ret = "connect() failed";
163        goto error;
164    }
165    FF_DEBUG("connect() succeeded");
166
167    FF_DEBUG("Attempting to send %u bytes of data...", state->command.length);
168    if (send(state->sockfd, state->command.chars, state->command.length, 0) < 0) {
169        FF_DEBUG("send() failed: %s", strerror(errno));
170        ret = "send() failed";
171        goto error;
172    }
173    FF_DEBUG("Data sent successfully");
174
175    goto exit;
176
177error:
178    FF_DEBUG("Error occurred, closing socket");
179    close(state->sockfd);
180    state->sockfd = -1;
181
182exit:
183    FF_DEBUG("Releasing address info and other resources");
184    freeaddrinfo(state->addr);
185    state->addr = nullptr;
186    ffStrbufDestroy(&state->command);
187
188    return ret;
189}
190
191FF_THREAD_ENTRY_DECL_WRAPPER(connectAndSend, FFNetworkingState*);
192
193// Parallel DNS resolution and socket creation
194static const char* initNetworkingState(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
195    FF_DEBUG("Initializing network connection state: host=%s, path=%s", host, path);
196
197    // Initialize command and host information
198    ffStrbufInitA(&state->command, 128);
199    ffStrbufAppendS(&state->command, "GET ");
200    ffStrbufAppendS(&state->command, path);
201    ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: ");
202    if (strchr(host, ':') != nullptr) {
203        // An IPv6 literal has to be bracketed in the Host header (RFC 9110 7.2), while
204        // getaddrinfo() wants it bare
205        ffStrbufAppendC(&state->command, '[');
206        ffStrbufAppendS(&state->command, host);
207        ffStrbufAppendC(&state->command, ']');
208    } else {
209        ffStrbufAppendS(&state->command, host);
210    }
211    // The Host header carries the port whenever it is not the default one (RFC 9110 7.2)
212    if (port != 80) {
213        ffStrbufAppendF(&state->command, ":%u", port);
214    }
215    ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly tell the server we don't need to keep the connection
216
217    ffStrbufAppendS(&state->command, headers);
218    ffStrbufAppendS(&state->command, "\r\n");
219
220#ifdef FF_HAVE_THREADS
221    state->thread = 0;
222    FF_DEBUG("Thread ID initialized to 0");
223#endif
224
225    const char* ret = nullptr;
226
227    struct addrinfo hints = {
228        .ai_family = state->ipv6 ? AF_INET6 : AF_INET,
229        .ai_socktype = SOCK_STREAM,
230        .ai_flags = AI_NUMERICSERV
231    };
232
233    char portA[6];
234    snprintf(portA, sizeof(portA), "%u", port);
235
236    FF_DEBUG("Resolving address: %s:%u (%s)", host, port, state->ipv6 ? "IPv6" : "IPv4");
237    // Use AI_NUMERICSERV flag to indicate the service is a numeric port, reducing parsing time
238
239    int gaiRes = getaddrinfo(host, portA, &hints, &state->addr);
240    if (gaiRes != 0) {
241        FF_DEBUG("getaddrinfo() failed: %s (res=%d)", gai_strerror(gaiRes), gaiRes);
242        ret = "getaddrinfo() failed";
243        goto error;
244    }
245    FF_DEBUG("Address resolution successful");
246
247    FF_DEBUG("Creating socket");
248    state->sockfd = socket(state->addr->ai_family, state->addr->ai_socktype, state->addr->ai_protocol);
249    if (state->sockfd == -1) {
250        FF_DEBUG("socket() failed: %s", strerror(errno));
251        ret = "socket() failed";
252        goto error;
253    }
254    FF_DEBUG("Socket creation successful: fd=%d", state->sockfd);
255
256    int flag = 1;
257#ifdef TCP_NODELAY
258    // Disable Nagle's algorithm to reduce small packet transmission delay
259    if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)) != 0) {
260        FF_DEBUG("Failed to set TCP_NODELAY: %s", strerror(errno));
261    } else {
262        FF_DEBUG("Successfully disabled Nagle's algorithm");
263    }
264#endif
265
266#ifdef TCP_QUICKACK
267    // Set TCP_QUICKACK option to avoid delayed acknowledgments
268    if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_QUICKACK, &flag, sizeof(flag)) != 0) {
269        FF_DEBUG("Failed to set TCP_QUICKACK: %s", strerror(errno));
270    } else {
271        FF_DEBUG("Successfully enabled TCP quick acknowledgment");
272    }
273#endif
274
275#ifdef SO_NOSIGPIPE
276    // Prevent SIGPIPE when the server closes the connection during write
277    if (setsockopt(state->sockfd, SOL_SOCKET, SO_NOSIGPIPE, &flag, sizeof(flag)) != 0) {
278        FF_DEBUG("Failed to set SO_NOSIGPIPE: %s", strerror(errno));
279    } else {
280        FF_DEBUG("Successfully set SO_NOSIGPIPE");
281    }
282#endif
283
284    if (state->timeout > 0) {
285        FF_DEBUG("Setting connection timeout: %u ms", state->timeout);
286        [[maybe_unused]] uint32_t sec = state->timeout / 1000;
287        if (sec == 0) {
288            sec = 1;
289        }
290
291#ifdef TCP_CONNECTIONTIMEOUT
292        FF_DEBUG("Using TCP_CONNECTIONTIMEOUT: %u seconds", sec);
293        setsockopt(state->sockfd, IPPROTO_TCP, TCP_CONNECTIONTIMEOUT, &sec, sizeof(sec));
294#elif defined(TCP_KEEPINIT)
295        FF_DEBUG("Using TCP_KEEPINIT: %u seconds", sec);
296        setsockopt(state->sockfd, IPPROTO_TCP, TCP_KEEPINIT, &sec, sizeof(sec));
297#elif defined(TCP_USER_TIMEOUT)
298        FF_DEBUG("Using TCP_USER_TIMEOUT: %u milliseconds", state->timeout);
299        setsockopt(state->sockfd, IPPROTO_TCP, TCP_USER_TIMEOUT, &state->timeout, sizeof(state->timeout));
300#else
301        FF_DEBUG("Current platform does not support TCP connection timeout");
302#endif
303    }
304
305    return nullptr;
306
307error:
308    FF_DEBUG("Error occurred during initialization");
309    if (state->addr != nullptr) {
310        FF_DEBUG("Releasing address information");
311        freeaddrinfo(state->addr);
312        state->addr = nullptr;
313    }
314
315    if (state->sockfd > 0) {
316        FF_DEBUG("Closing socket: fd=%d", state->sockfd);
317        close(state->sockfd);
318        state->sockfd = -1;
319    }
320
321    ffStrbufClear(&state->command);
322    return ret;
323}
324
325const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
326    FF_DEBUG("Preparing to send HTTP request: host=%s, port=%u, path=%s", host, port, path);
327
328    const char* initResult = initNetworkingState(state, host, port, path, headers);
329    if (initResult != nullptr) {
330        FF_DEBUG("Initialization failed: %s", initResult);
331        return initResult;
332    }
333    FF_DEBUG("Network state initialization successful");
334
335    const char* tfoResult = tryNonThreadingFastPath(state);
336    if (tfoResult == nullptr) {
337        FF_DEBUG("TryNonThreadingFastPath() succeeded or in progress");
338        return nullptr;
339    }
340    FF_DEBUG("TryNonThreadingFastPath() failed: %s, trying traditional connection", tfoResult);
341
342#ifdef FF_HAVE_THREADS
343    if (instance.config.general.multithreading) {
344        FF_DEBUG("Multithreading mode enabled, creating connection thread");
345        state->thread = ffThreadCreate(connectAndSendThreadMain, state);
346        if (state->thread) {
347            FF_DEBUG("Thread creation successful: thread=%p", (void*) (uintptr_t) state->thread);
348            return nullptr;
349        }
350        FF_DEBUG("Thread creation failed");
351    } else {
352        FF_DEBUG("Multithreading mode disabled, connecting in main thread");
353    }
354#endif
355
356    return connectAndSend(state);
357}
358
359const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) {
360    assert(buffer->allocated > 0);
361    FF_DEBUG("Preparing to receive HTTP response");
362    uint32_t timeout = state->timeout;
363
364#ifdef FF_HAVE_THREADS
365    if (state->thread) {
366        FF_DEBUG("Connection thread is running, waiting for it to complete (timeout=%u ms)", timeout);
367        if (!ffThreadJoin(state->thread, timeout)) {
368            FF_DEBUG("Thread join failed or timed out");
369            return "ffThreadJoin() failed or timeout";
370        }
371        FF_DEBUG("Thread completed successfully");
372        state->thread = 0;
373    }
374#endif
375
376    if (state->sockfd == -1) {
377        FF_DEBUG("Invalid socket, HTTP request might have failed");
378        return "ffNetworkingSendHttpRequest() failed";
379    }
380
381    // Set larger initial receive buffer instead of small repeated receives
382    int rcvbuf = 64 * 1024;
383    setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
384
385    // The timeout has to be enforced by the socket itself on every platform: the poll() below
386    // only reports readability once, so a server that sends a partial response and then keeps
387    // the connection open would otherwise block this loop forever.
388    if (timeout > 0) {
389        FF_DEBUG("Setting receive timeout: %u ms", timeout);
390        struct timeval timev;
391        timev.tv_sec = timeout / 1000;
392        timev.tv_usec = (typeof(timev.tv_usec)) ((timeout % 1000) * 1000); // milliseconds to microseconds
393        setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, &timev, sizeof(timev));
394    }
395
396#ifdef __APPLE__
397    // poll for the socket to be readable.
398    // Because of the non-blocking connectx() call, the connection might not be established yet
399    FF_DEBUG("Using poll() to check if socket is readable");
400    {
401        int pollRes = poll(&(struct pollfd) {
402                               .fd = state->sockfd,
403                               .events = POLLIN },
404            1,
405            timeout > 0 ? (int) timeout : -1);
406        if (pollRes == 0) {
407            FF_DEBUG("poll() timed out after %u ms", timeout);
408            close(state->sockfd);
409            state->sockfd = -1;
410            return "poll() timeout";
411        } else if (pollRes == -1) {
412            FF_DEBUG("poll() failed: %s", strerror(errno));
413            close(state->sockfd);
414            state->sockfd = -1;
415            return "poll() failed";
416        }
417    }
418    FF_DEBUG("Socket is readable, proceeding to receive data");
419#endif
420
421    if (shutdown(state->sockfd, SHUT_WR) == -1) {
422        FF_DEBUG("Failed to shutdown socket send: %s", strerror(errno));
423        // Not a critical error, continue anyway
424    }
425
426    FF_DEBUG("Starting data reception");
427    [[maybe_unused]] int recvCount = 0;
428    uint32_t contentLength = 0;
429    uint32_t headerEnd = 0;
430    bool chunked = false;
431
432    // Runs until the response is framed; the buffer is grown on demand at the top
433    for (;;) {
434        if (ffStrbufGetFree(buffer) == 0) {
435            // `Content-Length` may be absent (e.g. chunked responses). Grow the buffer
436            // on demand instead of silently truncating the response.
437            if (buffer->allocated >= FF_NETWORKING_MAX_RESPONSE_SIZE) {
438                FF_DEBUG("Response is too large: %u bytes, aborting", buffer->allocated);
439                close(state->sockfd);
440                state->sockfd = -1;
441                return "Response too large";
442            }
443            FF_DEBUG("Receive buffer is full, extending it");
444            // Asking for exactly the room that is left under the cap, rather than for as much as
445            // the buffer holds again: the allocation is rounded up to a power of two, so a doubling
446            // from a size that is not one of those lands past the cap the check above just cleared.
447            ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
448        }
449
450        // When the remaining length is known, ask for exactly that much. MSG_WAITALL then
451        // returns as soon as the response is complete, instead of waiting for the server
452        // to close the connection.
453        // Without a Content-Length the requested length is just "whatever fits", so
454        // waiting for all of it would block until the server closes -- and the framing
455        // checks below would never get a chance to run. Read whatever has arrived instead
456        // and let those checks decide when the response is complete.
457        uint32_t want = ffStrbufGetFree(buffer);
458        int recvFlags = 0;
459        if (contentLength > 0 && headerEnd > 0) {
460            uint32_t remaining = headerEnd + 4 + contentLength - buffer->length;
461            if (remaining < want) {
462                want = remaining;
463            }
464            recvFlags = MSG_WAITALL;
465        }
466
467        FF_DEBUG("Data reception loop #%d, current buffer size: %u, requesting %u bytes",
468            ++recvCount,
469            buffer->length,
470            want);
471
472        ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, want, recvFlags);
473
474        if (received <= 0) {
475            if (received == 0) {
476                FF_DEBUG("Connection closed (received=0)");
477            } else {
478                FF_DEBUG("Reception failed: %s", strerror(errno));
479            }
480            break;
481        }
482
483        buffer->length += (uint32_t) received;
484        buffer->chars[buffer->length] = '\0';
485
486        FF_DEBUG("Successfully received %zd bytes of data, total: %u bytes", received, buffer->length);
487
488        // Check if HTTP header end marker is found
489        if (headerEnd == 0) {
490            char* pHeaderEnd = memmem(buffer->chars, buffer->length, "\r\n\r\n", 4);
491            if (pHeaderEnd) {
492                headerEnd = (uint32_t) (pHeaderEnd - buffer->chars);
493                FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd);
494
495                // Check for Content-Length header to pre-allocate enough memory
496                uint32_t valueLen = 0;
497                const char* clHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Content-Length:", &valueLen);
498                if (clHeader) {
499                    contentLength = (uint32_t) strtoul(clHeader, nullptr, 10);
500                    if (contentLength > 0) {
501                        if (contentLength > FF_NETWORKING_MAX_RESPONSE_SIZE) { // 1MB limit to prevent excessive memory allocation and potential attacks
502                            FF_DEBUG("Content-Length is too large: %u bytes, aborting", contentLength);
503                            close(state->sockfd);
504                            state->sockfd = -1;
505                            return "Content-Length too large";
506                        }
507
508                        FF_DEBUG("Detected Content-Length: %u, pre-allocating buffer", contentLength);
509                        // Ensure buffer is large enough, adding header size and some margin
510                        ffStrbufEnsureFree(buffer, contentLength + 16);
511                        FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated);
512                    }
513                }
514
515                // A chunked response has no Content-Length; it is framed by a last-chunk
516                const char* teHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Transfer-Encoding:", &valueLen);
517                if (teHeader != nullptr) {
518                    switch (ffNetworkingParseTransferEncoding(teHeader, valueLen)) {
519                        case FF_NETWORKING_TE_CHUNKED:
520                            FF_DEBUG("Detected chunked transfer encoding");
521                            chunked = true;
522                            break;
523                        case FF_NETWORKING_TE_UNSUPPORTED:
524                            // The framing of e.g. `gzip, chunked` is unreadable and the payload
525                            // would stay encoded, so fail instead of returning garbage
526                            FF_DEBUG("Unsupported Transfer-Encoding: %.*s", (int) valueLen, teHeader);
527                            close(state->sockfd);
528                            state->sockfd = -1;
529                            return "Unsupported Transfer-Encoding";
530                        default:
531                            break;
532                    }
533                }
534            }
535        }
536
537        // Stop as soon as the response is framed, rather than waiting for the FIN
538        if (chunked) {
539            uint32_t consumed = 0;
540            int complete = ffNetworkingChunkedComplete(buffer->chars + headerEnd + 4, buffer->length - headerEnd - 4, &consumed);
541            if (complete < 0) {
542                FF_DEBUG("Malformed chunked body");
543                close(state->sockfd);
544                state->sockfd = -1;
545                return "Malformed chunked body";
546            }
547            if (complete > 0) {
548                FF_DEBUG("Chunked body complete, %u bytes of encoded body", consumed);
549                break;
550            }
551        } else if (contentLength > 0 && buffer->length >= headerEnd + 4 + contentLength) {
552            break;
553        }
554    }
555
556    FF_DEBUG("Closing socket: fd=%d", state->sockfd);
557    close(state->sockfd);
558    state->sockfd = -1;
559
560    if (buffer->length == 0) {
561        FF_DEBUG("Server response is empty");
562        return "Empty server response received";
563    }
564
565    if (headerEnd == 0) {
566        FF_DEBUG("No HTTP header end marker found");
567        return "No HTTP header end found";
568    }
569
570    if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
571        FF_DEBUG("Failed to decode chunked response");
572        return "Failed to decode chunked response";
573    }
574
575    if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n") && !ffStrbufStartsWithS(buffer, "HTTP/1.1 200 OK\r\n")) {
576        FF_DEBUG("Invalid response: %.40s...", buffer->chars);
577        return "Invalid response";
578    }
579    FF_DEBUG("Received valid HTTP 200 response, content %u bytes, total %u bytes", contentLength, buffer->length);
580
581    // A chunked response was framed by its last-chunk and has been rewritten around it, so any
582    // `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
583    // says not to send both; when one does, the chunked framing is the one that was acted on.
584    if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
585        FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
586        return "Content length mismatch";
587    }
588
589    return nullptr;
590}