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}