master ohmu/fastfetch-openbsd / src / common / impl / networking_windows.c
ohmu  ·  2026-10-15
  1#include <mswsock.h>
  2#include <ws2tcpip.h>
  3
  4// Must be included after <mswsock.h>
  5#include "fastfetch.h"
  6#include "common/networking.h"
  7#include "common/strutil.h"
  8#include "common/debug.h"
  9
 10static LPFN_CONNECTEX ConnectEx;
 11
 12// Upper bound of a single HTTP response, guarding against excessive memory allocation
 13#define FF_NETWORKING_MAX_RESPONSE_SIZE (1024u * 1024u)
 14
 15static const char* initWsaData(WSADATA* wsaData) {
 16    FF_DEBUG("Initializing WinSock");
 17    int ret = WSAStartup(MAKEWORD(2, 2), wsaData);
 18    if (ret != 0) {
 19        FF_DEBUG("WSAStartup() failed: %s", ffDebugWin32Error((DWORD) ret));
 20        return "WSAStartup() failed";
 21    }
 22
 23    if (LOBYTE(wsaData->wVersion) != 2 || HIBYTE(wsaData->wVersion) != 2) {
 24        FF_DEBUG("Invalid wsaData version found: %d.%d", LOBYTE(wsaData->wVersion), HIBYTE(wsaData->wVersion));
 25        WSACleanup();
 26        return "Invalid wsaData version found";
 27    }
 28
 29    // Dummy socket needed for WSAIoctl
 30    SOCKET sockfd = WSASocketW(AF_INET, SOCK_STREAM, 0, nullptr, 0, 0);
 31    if (sockfd == INVALID_SOCKET) {
 32        FF_DEBUG("WSASocketW(AF_INET, SOCK_STREAM) failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
 33        WSACleanup();
 34        return "WSASocketW(AF_INET, SOCK_STREAM) failed";
 35    }
 36
 37    DWORD dwBytes;
 38    GUID guid = WSAID_CONNECTEX;
 39    if (WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &ConnectEx, sizeof(ConnectEx), &dwBytes, nullptr, nullptr) != 0) {
 40        FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
 41        closesocket(sockfd);
 42        WSACleanup();
 43        return "WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed";
 44    }
 45
 46    closesocket(sockfd);
 47    FF_DEBUG("WinSock initialized successfully");
 48
 49    return nullptr;
 50}
 51
 52const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers) {
 53    FF_DEBUG("Preparing to send HTTP request: host=%s, port=%u, path=%s", host, port, path);
 54
 55    static WSADATA wsaData;
 56    if (wsaData.wVersion == 0) {
 57        const char* error = initWsaData(&wsaData);
 58        if (error != nullptr) {
 59            wsaData.wVersion = (WORD) -1;
 60            FF_DEBUG("WinSock initialization failed: %s", error);
 61            return error;
 62        }
 63    } else if (wsaData.wVersion == (WORD) -1) {
 64        FF_DEBUG("WinSock initialization previously failed");
 65        return "initWsaData() failed before";
 66    }
 67
 68    ADDRINFOW* addr;
 69    ADDRINFOW hints = {
 70        .ai_flags = AI_NUMERICSERV,
 71        .ai_family = state->ipv6 ? AF_INET6 : AF_INET,
 72        .ai_socktype = SOCK_STREAM,
 73    };
 74
 75    wchar_t hostW[256];
 76    NTSTATUS status = RtlUTF8ToUnicodeN(hostW, (ULONG) sizeof(hostW), nullptr, host, (ULONG) strlen(host) + 1);
 77    if (!NT_SUCCESS(status)) {
 78        FF_DEBUG("Failed to convert host to wide string: %s: %s", host, ffDebugNtStatus(status));
 79        return "Failed to convert host to wide string";
 80    }
 81
 82    wchar_t portW[6];
 83    _itow(port, portW, 10);
 84
 85    FF_DEBUG("Resolving address: %s:%u (%s)", host, port, state->ipv6 ? "IPv6" : "IPv4");
 86    int ret = GetAddrInfoW(hostW, portW, &hints, &addr);
 87    if (ret != 0) {
 88        FF_DEBUG("GetAddrInfoW() failed: %s", ffDebugWin32Error((DWORD) ret));
 89        return "GetAddrInfoW() failed";
 90    }
 91
 92    state->sockfd = WSASocketW(addr->ai_family, addr->ai_socktype, addr->ai_protocol, nullptr, 0, 0);
 93    if (state->sockfd == INVALID_SOCKET) {
 94        FF_DEBUG("WSASocketW() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
 95        FreeAddrInfoW(addr);
 96        return "WSASocketW() failed";
 97    }
 98
 99    DWORD flag = 1;
100#ifdef TCP_NODELAY
101    // Enable TCP_NODELAY to disable Nagle's algorithm
102    if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)) != 0) {
103        FF_DEBUG("Failed to set TCP_NODELAY: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
104    } else {
105        FF_DEBUG("Successfully disabled Nagle's algorithm");
106    }
107#endif
108
109    // Set timeout if needed
110    if (state->timeout > 0) {
111        FF_DEBUG("Setting connection timeout: %u ms", state->timeout);
112        setsockopt(state->sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char*) &state->timeout, sizeof(state->timeout));
113    }
114
115    // ConnectEx requires the socket to be initially bound
116    if ((state->ipv6
117                ? bind(state->sockfd, (SOCKADDR*) &(struct sockaddr_in6) {
118                                          .sin6_family = AF_INET6,
119                                          .sin6_addr = in6addr_any,
120                                      },
121                      sizeof(struct sockaddr_in6))
122                : bind(state->sockfd, (SOCKADDR*) &(struct sockaddr_in) {
123                                          .sin_family = AF_INET,
124                                          .sin_addr.s_addr = INADDR_ANY,
125                                      },
126                      sizeof(struct sockaddr_in))) != 0) {
127        FF_DEBUG("bind() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
128        closesocket(state->sockfd);
129        FreeAddrInfoW(addr);
130        state->sockfd = INVALID_SOCKET;
131        return "bind() failed";
132    }
133
134    // Initialize overlapped structure with WSA event for asynchronous I/O
135    state->overlapped = (OVERLAPPED) {};
136
137    NTSTATUS eventStatus = NtCreateEvent(&state->overlapped.hEvent, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE);
138    if (!NT_SUCCESS(eventStatus)) {
139        FF_DEBUG("NtCreateEvent() failed: %s", ffDebugNtStatus(eventStatus));
140        closesocket(state->sockfd);
141        FreeAddrInfoW(addr);
142        state->sockfd = INVALID_SOCKET;
143        return "NtCreateEvent() failed";
144    }
145
146    // Build HTTP command
147    ffStrbufInitA(&state->command, 128);
148    ffStrbufAppendS(&state->command, "GET ");
149    ffStrbufAppendS(&state->command, path);
150    ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: ");
151    if (strchr(host, ':') != nullptr) {
152        // An IPv6 literal has to be bracketed in the Host header (RFC 9110 7.2), while
153        // GetAddrInfoW() wants it bare
154        ffStrbufAppendC(&state->command, '[');
155        ffStrbufAppendS(&state->command, host);
156        ffStrbufAppendC(&state->command, ']');
157    } else {
158        ffStrbufAppendS(&state->command, host);
159    }
160    // The Host header carries the port whenever it is not the default one (RFC 9110 7.2)
161    if (port != 80) {
162        ffStrbufAppendF(&state->command, ":%u", port);
163    }
164    ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly request connection closure
165    ffStrbufAppendS(&state->command, headers);
166    ffStrbufAppendS(&state->command, "\r\n");
167
168#ifdef TCP_FASTOPEN
169    if (state->tfo) {
170        // Set TCP Fast Open
171        flag = 1;
172        if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_FASTOPEN, (char*) &flag, sizeof(flag)) != 0) {
173            FF_DEBUG("Failed to set TCP_FASTOPEN option: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
174        } else {
175            FF_DEBUG("Successfully set TCP_FASTOPEN option");
176        }
177    } else {
178        FF_DEBUG("TCP Fast Open disabled");
179    }
180#endif
181
182    FF_DEBUG("Using ConnectEx to send %u bytes of data", state->command.length);
183    DWORD sent = 0;
184    BOOL result = ConnectEx(state->sockfd, addr->ai_addr, (int) addr->ai_addrlen, state->command.chars, state->command.length, &sent, &state->overlapped);
185
186    FreeAddrInfoW(addr);
187    addr = nullptr;
188
189    if (!result) {
190        if (WSAGetLastError() != WSA_IO_PENDING) {
191            FF_DEBUG("ConnectEx() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
192            NtClose(state->overlapped.hEvent);
193            closesocket(state->sockfd);
194            state->sockfd = INVALID_SOCKET;
195            ffStrbufDestroy(&state->command);
196            return "ConnectEx() failed";
197        } else {
198            FF_DEBUG("ConnectEx() pending");
199        }
200    } else {
201        FF_DEBUG("ConnectEx() succeeded, sent %u bytes of data", (unsigned) sent);
202    }
203
204    // No need to cleanup state fields here since we need them in the receive function
205    return nullptr;
206}
207
208const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) {
209    assert(buffer->allocated > 0);
210    FF_DEBUG("Preparing to receive HTTP response");
211
212    if (state->sockfd == INVALID_SOCKET) {
213        FF_DEBUG("Invalid socket, HTTP request might have failed");
214        return "ffNetworkingSendHttpRequest() failed";
215    }
216
217    DWORD transfer;
218    uint32_t timeout = state->timeout;
219    if (!GetOverlappedResultEx((HANDLE) state->sockfd, &state->overlapped, &transfer, timeout > 0 ? timeout : INFINITE, FALSE)) {
220        DWORD error = GetLastError();
221        if (error == WAIT_TIMEOUT) {
222            FF_DEBUG("GetOverlappedResultEx timed out");
223        } else {
224            FF_DEBUG("GetOverlappedResultEx failed: %s", ffDebugWin32Error(error));
225        }
226        IO_STATUS_BLOCK cancelIosb = {};
227        if (NT_SUCCESS(NtCancelIoFileEx((HANDLE) state->sockfd, (PIO_STATUS_BLOCK) &state->overlapped, &cancelIosb))) {
228            NtWaitForSingleObject(state->overlapped.hEvent, TRUE, &(LARGE_INTEGER) { .QuadPart = (int64_t) 10 * -10000 });
229        }
230        NtClose(state->overlapped.hEvent);
231        closesocket(state->sockfd);
232        ffStrbufDestroy(&state->command);
233        return "GetOverlappedResultEx() failed or timeout";
234    }
235    FF_DEBUG("GetOverlappedResultEx succeeded, %u bytes sent", (unsigned) transfer);
236    ffStrbufDestroy(&state->command);
237    NtClose(state->overlapped.hEvent);
238    state->overlapped.hEvent = nullptr;
239
240    if (setsockopt(state->sockfd, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, nullptr, 0) != 0) {
241        FF_DEBUG("Failed to update connect context: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
242        // Not a critical error, continue anyway
243    }
244
245    if (shutdown(state->sockfd, SD_SEND) == SOCKET_ERROR) {
246        FF_DEBUG("Failed to shutdown socket send: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
247        // Not a critical error, continue anyway
248    }
249
250    if (timeout > 0) {
251        FF_DEBUG("Setting receive timeout: %u ms", timeout);
252        setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*) &timeout, sizeof(timeout));
253    }
254
255    // Set larger receive buffer for better performance
256    int rcvbuf = 65536; // 64KB
257    if (setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, (const char*) &rcvbuf, sizeof(rcvbuf))) {
258        FF_DEBUG("Failed to set SO_RCVBUF: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
259        // Not a critical error, continue anyway
260    }
261
262    FF_DEBUG("Starting data reception");
263    [[maybe_unused]] int recvCount = 0;
264    uint32_t contentLength = 0;
265    uint32_t headerEnd = 0;
266    bool chunked = false;
267
268    // Runs until the response is framed; the buffer is grown on demand at the top
269    for (;;) {
270        if (ffStrbufGetFree(buffer) == 0) {
271            // `Content-Length` may be absent (e.g. chunked responses). Grow the buffer
272            // on demand instead of silently truncating the response.
273            if (buffer->allocated >= FF_NETWORKING_MAX_RESPONSE_SIZE) {
274                FF_DEBUG("Response is too large: %u bytes, aborting", buffer->allocated);
275                closesocket(state->sockfd);
276                state->sockfd = INVALID_SOCKET;
277                return "Response too large";
278            }
279            FF_DEBUG("Receive buffer is full, extending it");
280            // Asking for exactly the room that is left under the cap, rather than for as much as
281            // the buffer holds again: the allocation is rounded up to a power of two, so a doubling
282            // from a size that is not one of those lands past the cap the check above just cleared.
283            ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
284        }
285
286        FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u",
287            ++recvCount,
288            buffer->length,
289            ffStrbufGetFree(buffer));
290
291        DWORD received = 0, recvFlags = 0;
292        int recvResult = WSARecv(state->sockfd, &(WSABUF) {
293                                                    .buf = buffer->chars + buffer->length,
294                                                    .len = (ULONG) ffStrbufGetFree(buffer),
295                                                },
296            1,
297            &received,
298            &recvFlags,
299            nullptr,
300            nullptr);
301
302        if (recvResult == SOCKET_ERROR || received == 0) {
303            if (recvResult == 0 && received == 0) {
304                FF_DEBUG("Connection closed (received=0)");
305            } else {
306                FF_DEBUG("Reception failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
307            }
308            break;
309        }
310
311        buffer->length += (uint32_t) received;
312        buffer->chars[buffer->length] = '\0';
313
314        FF_DEBUG("Successfully received %u bytes of data, total: %u bytes", (unsigned) received, buffer->length);
315
316        // Check if HTTP header end marker is found
317        if (headerEnd == 0) {
318            char* pHeaderEnd = strstr(buffer->chars, "\r\n\r\n");
319            if (pHeaderEnd) {
320                headerEnd = (uint32_t) (pHeaderEnd - buffer->chars);
321                FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd);
322
323                // Check for Content-Length header to pre-allocate enough memory
324                uint32_t valueLen = 0;
325                const char* clHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Content-Length:", &valueLen);
326                if (clHeader) {
327                    contentLength = (uint32_t) strtoul(clHeader, nullptr, 10);
328                    if (contentLength > 0) {
329                        if (contentLength > FF_NETWORKING_MAX_RESPONSE_SIZE) { // 1MB limit to prevent excessive memory allocation and potential attacks
330                            FF_DEBUG("Content-Length is too large: %u bytes, aborting", contentLength);
331                            closesocket(state->sockfd);
332                            state->sockfd = INVALID_SOCKET;
333                            return "Content-Length too large";
334                        }
335
336                        FF_DEBUG("Detected Content-Length: %u, pre-allocating buffer", contentLength);
337                        // Ensure buffer is large enough, adding header size and some margin
338                        ffStrbufEnsureFree(buffer, contentLength + 16);
339                        FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated);
340                    }
341                }
342
343                // A chunked response has no Content-Length; it is framed by a last-chunk
344                const char* teHeader = ffNetworkingFindHeader(buffer->chars, headerEnd, "Transfer-Encoding:", &valueLen);
345                if (teHeader != nullptr) {
346                    switch (ffNetworkingParseTransferEncoding(teHeader, valueLen)) {
347                        case FF_NETWORKING_TE_CHUNKED:
348                            FF_DEBUG("Detected chunked transfer encoding");
349                            chunked = true;
350                            break;
351                        case FF_NETWORKING_TE_UNSUPPORTED:
352                            // The framing of e.g. `gzip, chunked` is unreadable and the payload
353                            // would stay encoded, so fail instead of returning garbage
354                            FF_DEBUG("Unsupported Transfer-Encoding: %.*s", (int) valueLen, teHeader);
355                            closesocket(state->sockfd);
356                            state->sockfd = INVALID_SOCKET;
357                            return "Unsupported Transfer-Encoding";
358                        default:
359                            break;
360                    }
361                }
362            }
363        }
364
365        // Stop as soon as the response is framed, rather than waiting for the FIN
366        if (chunked) {
367            uint32_t consumed = 0;
368            int complete = ffNetworkingChunkedComplete(buffer->chars + headerEnd + 4, buffer->length - headerEnd - 4, &consumed);
369            if (complete < 0) {
370                FF_DEBUG("Malformed chunked body");
371                closesocket(state->sockfd);
372                state->sockfd = INVALID_SOCKET;
373                return "Malformed chunked body";
374            }
375            if (complete > 0) {
376                FF_DEBUG("Chunked body complete, %u bytes of encoded body", consumed);
377                break;
378            }
379        } else if (contentLength > 0 && buffer->length >= headerEnd + 4 + contentLength) {
380            break;
381        }
382    }
383
384    FF_DEBUG("Closing socket: fd=%u", (unsigned) state->sockfd);
385    closesocket(state->sockfd);
386    state->sockfd = INVALID_SOCKET;
387
388    if (buffer->length == 0) {
389        FF_DEBUG("Server response is empty");
390        return "Empty server response received";
391    }
392
393    if (headerEnd == 0) {
394        FF_DEBUG("No HTTP header end marker found");
395        return "No HTTP header end found";
396    }
397
398    if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
399        FF_DEBUG("Failed to decode chunked response");
400        return "Failed to decode chunked response";
401    }
402
403    if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n") && !ffStrbufStartsWithS(buffer, "HTTP/1.1 200 OK\r\n")) {
404        FF_DEBUG("Invalid response: %.40s...", buffer->chars);
405        return "Invalid response";
406    }
407    FF_DEBUG("Received valid HTTP 200 response, content length: %u bytes, total length: %u bytes",
408        contentLength,
409        buffer->length);
410
411    // A chunked response was framed by its last-chunk and has been rewritten around it, so any
412    // `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
413    // says not to send both; when one does, the chunked framing is the one that was acted on.
414    if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
415        FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
416        return "Content length mismatch";
417    }
418
419    return nullptr;
420}