diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d1ade90e..6ac15a783 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -364,6 +364,7 @@ set(LIBFASTFETCH_SRC src/common/library.c src/common/modules.c src/common/netif/netif.c + src/common/networking_common.c src/common/option.c src/common/parsing.c src/common/printing.c diff --git a/src/common/networking.h b/src/common/networking.h index a11114e35..338827eaa 100644 --- a/src/common/networking.h +++ b/src/common/networking.h @@ -31,3 +31,8 @@ typedef struct FFNetworkingState { const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers); const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer); + +#ifdef FF_HAVE_ZLIB +const char* ffNetworkingLoadZlibLibrary(void); +bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd); +#endif diff --git a/src/common/networking_common.c b/src/common/networking_common.c new file mode 100644 index 000000000..769860ff7 --- /dev/null +++ b/src/common/networking_common.c @@ -0,0 +1,188 @@ +#include "fastfetch.h" +#include "common/library.h" +#include "common/networking.h" +#include "util/stringUtils.h" +#include "util/debug.h" + +#ifdef FF_HAVE_ZLIB +#include + +struct FFZlibLibrary +{ + FF_LIBRARY_SYMBOL(inflateInit2_) + FF_LIBRARY_SYMBOL(inflate) + FF_LIBRARY_SYMBOL(inflateEnd) + FF_LIBRARY_SYMBOL(inflateGetHeader) + + bool inited; +} zlibData; + +const char* ffNetworkingLoadZlibLibrary(void) +{ + if (!zlibData.inited) + { + zlibData.inited = true; + FF_LIBRARY_LOAD(zlib, "dlopen libz failed", "libz" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateInit2_) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflate) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateEnd) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateGetHeader) + zlib = NULL; // don't auto dlclose + } + return zlibData.ffinflateEnd == NULL ? "Failed to load libz" : NULL; +} + +// Try to pre-read gzip header to determine uncompressed size +static uint32_t guessGzipOutputSize(const void* data, uint32_t dataSize) +{ + // gzip file format: http://www.zlib.org/rfc-gzip.html + if (dataSize < 10 || ((const uint8_t*)data)[0] != 0x1f || ((const uint8_t*)data)[1] != 0x8b) + return 0; + + // Uncompressed size in gzip format is stored in the last 4 bytes, but only valid if data is less than 4GB + if (dataSize > 18) { + // Get ISIZE value from the end of file (little endian) + const uint8_t* tail = (const uint8_t*)data + dataSize - 4; + uint32_t uncompressedSize = tail[0] | (tail[1] << 8) | (tail[2] << 16) | (tail[3] << 24); + + // For valid gzip files, this value is the length of the uncompressed data modulo 2^32 + if (uncompressedSize > 0) { + FF_DEBUG("Read uncompressed size from GZIP trailer: %u bytes", uncompressedSize); + // Add some margin to the estimated size for safety + return uncompressedSize + 64; + } + } + + // If unable to get size from trailer or size is 0, use estimated value + // Typically, text data compression ratio is between 3-5x, we use the larger value + uint32_t estimatedSize = dataSize * 5; + FF_DEBUG("Unable to read exact uncompressed size, estimated as 5x of compressed data: %u bytes", estimatedSize); + return estimatedSize; +} + +// Decompress gzip content +bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) +{ + // Calculate header size + uint32_t headerSize = (uint32_t) (headerEnd - buffer->chars); + + *headerEnd = '\0'; // Replace delimiter with null character for easier processing + // Ensure Content-Encoding is in response headers, not in response body + bool hasGzip = strcasestr(buffer->chars, "\nContent-Encoding: gzip") != NULL; + *headerEnd = '\r'; // Restore delimiter + + if (!hasGzip) { + FF_DEBUG("No gzip compressed content detected, skipping decompression"); + return true; + } + + FF_DEBUG("Gzip compressed content detected, preparing for decompression"); + + const char* bodyStart = headerEnd + 4; // Skip delimiter + + // Calculate compressed content size + uint32_t compressedSize = buffer->length - headerSize - 4; + + if (compressedSize <= 0) { + // No content to decompress + FF_DEBUG("Compressed content size is 0, skipping decompression"); + return true; + } + + // Check if content is actually in gzip format (gzip header magic is 0x1f 0x8b) + if (compressedSize < 2 || (uint8_t)bodyStart[0] != 0x1f || (uint8_t)bodyStart[1] != 0x8b) { + FF_DEBUG("Content is not valid gzip format, skipping decompression"); + return false; + } + + // Predict uncompressed size + uint32_t estimatedSize = guessGzipOutputSize(bodyStart, compressedSize); + + // Create decompression buffer with estimated size + FF_STRBUF_AUTO_DESTROY decompressedBuffer = ffStrbufCreateA(estimatedSize > 0 ? estimatedSize : compressedSize * 5); + FF_DEBUG("Created decompression buffer: %u bytes", decompressedBuffer.allocated); + + // Initialize decompression + z_stream zs = { + .zalloc = Z_NULL, + .zfree = Z_NULL, + .opaque = Z_NULL, + .avail_in = (uInt)compressedSize, + .next_in = (Bytef*)bodyStart, + .avail_out = (uInt)ffStrbufGetFree(&decompressedBuffer), + .next_out = (Bytef*)decompressedBuffer.chars, + }; + + // Initialize decompression engine + if (zlibData.ffinflateInit2_(&zs, 16 + MAX_WBITS, ZLIB_VERSION, (int)sizeof(z_stream)) != Z_OK) { + FF_DEBUG("Failed to initialize decompression engine"); + return false; + } + uInt availableOut = zs.avail_out; + + // Perform decompression + int result = zlibData.ffinflate(&zs, Z_FINISH); + + // If output buffer is insufficient, try to extend buffer + while (result == Z_BUF_ERROR || (result != Z_STREAM_END && zs.avail_out == 0)) + { + FF_DEBUG("Output buffer insufficient, trying to extend"); + + // Save already decompressed data amount + uint32_t alreadyDecompressed = (uint32_t)(availableOut - zs.avail_out); + decompressedBuffer.length += alreadyDecompressed; + decompressedBuffer.chars[decompressedBuffer.length] = '\0'; // Ensure null-terminated string + + ffStrbufEnsureFree(&decompressedBuffer, decompressedBuffer.length / 2); + + // Set output parameters to point to new buffer + zs.avail_out = (uInt)ffStrbufGetFree(&decompressedBuffer); + zs.next_out = (Bytef*)(decompressedBuffer.chars + decompressedBuffer.length); + availableOut = zs.avail_out; + + // Decompress again + result = zlibData.ffinflate(&zs, Z_FINISH); + } + + zlibData.ffinflateEnd(&zs); + + // Calculate decompressed size + uint32_t decompressedSize = (uint32_t)(availableOut - zs.avail_out); + decompressedBuffer.length += decompressedSize; + decompressedBuffer.chars[decompressedSize] = '\0'; // Ensure null-terminated string + FF_DEBUG("Successfully decompressed %u bytes compressed data to %u bytes", compressedSize, decompressedBuffer.length); + + // Modify Content-Length header and remove Content-Encoding header + FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerSize + decompressedSize + 64); + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetline(&line, &len, buffer)) + { + if (ffStrStartsWithIgnCase(line, "Content-Encoding:")) + { + continue; + } + else if (ffStrStartsWithIgnCase(line, "Content-Length:")) + { + ffStrbufAppendF(&newBuffer, "Content-Length: %u\r\n", decompressedSize); + continue; + } + else if (line[0] == '\r') + { + ffStrbufAppendS(&newBuffer, "\r\n"); + ffStrbufGetlineRestore(&line, &len, buffer); + break; + } + + ffStrbufAppendS(&newBuffer, line); + ffStrbufAppendC(&newBuffer, '\n'); + } + + ffStrbufAppend(&newBuffer, &decompressedBuffer); + ffStrbufDestroy(buffer); + ffStrbufInitMove(buffer, &newBuffer); + + return true; +} +#endif // FF_HAVE_ZLIB diff --git a/src/common/networking_linux.c b/src/common/networking_linux.c index d945ea684..76ec71dbf 100644 --- a/src/common/networking_linux.c +++ b/src/common/networking_linux.c @@ -235,189 +235,6 @@ error: return ret; } -#ifdef FF_HAVE_ZLIB -#include - -struct FFZlibLibrary -{ - FF_LIBRARY_SYMBOL(inflateInit2_) - FF_LIBRARY_SYMBOL(inflate) - FF_LIBRARY_SYMBOL(inflateEnd) - FF_LIBRARY_SYMBOL(inflateGetHeader) - - bool inited; -} zlibData; - -static const char* loadZlibLibrary(void) -{ - if (!zlibData.inited) - { - zlibData.inited = true; - FF_LIBRARY_LOAD(zlib, "dlopen libz failed", "libz" FF_LIBRARY_EXTENSION, 2) - FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateInit2_) - FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflate) - FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateEnd) - FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateGetHeader) - zlib = NULL; // don't auto dlclose - } - return zlibData.ffinflateEnd == NULL ? "Failed to load libz" : NULL; -} - -// Try to pre-read gzip header to determine uncompressed size -static uint32_t guessGzipOutputSize(const void* data, uint32_t dataSize) -{ - // gzip file format: http://www.zlib.org/rfc-gzip.html - if (dataSize < 10 || ((const uint8_t*)data)[0] != 0x1f || ((const uint8_t*)data)[1] != 0x8b) - return 0; - - // Uncompressed size in gzip format is stored in the last 4 bytes, but only valid if data is less than 4GB - if (dataSize > 18) { - // Get ISIZE value from the end of file (little endian) - const uint8_t* tail = (const uint8_t*)data + dataSize - 4; - uint32_t uncompressedSize = tail[0] | (tail[1] << 8) | (tail[2] << 16) | (tail[3] << 24); - - // For valid gzip files, this value is the length of the uncompressed data modulo 2^32 - if (uncompressedSize > 0) { - FF_DEBUG("Read uncompressed size from GZIP trailer: %u bytes", uncompressedSize); - // Add some margin to the estimated size for safety - return uncompressedSize + 64; - } - } - - // If unable to get size from trailer or size is 0, use estimated value - // Typically, text data compression ratio is between 3-5x, we use the larger value - uint32_t estimatedSize = dataSize * 5; - FF_DEBUG("Unable to read exact uncompressed size, estimated as 5x of compressed data: %u bytes", estimatedSize); - return estimatedSize; -} - -// Decompress gzip content -static bool decompressGzip(FFstrbuf* buffer, char* headerEnd) -{ - // Calculate header size - uint32_t headerSize = (uint32_t) (headerEnd - buffer->chars); - - *headerEnd = '\0'; // Replace delimiter with null character for easier processing - // Ensure Content-Encoding is in response headers, not in response body - bool hasGzip = strcasestr(buffer->chars, "\nContent-Encoding: gzip") != NULL; - *headerEnd = '\r'; // Restore delimiter - - if (!hasGzip) { - FF_DEBUG("No gzip compressed content detected, skipping decompression"); - return true; - } - - FF_DEBUG("Gzip compressed content detected, preparing for decompression"); - - const char* bodyStart = headerEnd + 4; // Skip delimiter - - // Calculate compressed content size - uint32_t compressedSize = buffer->length - headerSize - 4; - - if (compressedSize <= 0) { - // No content to decompress - FF_DEBUG("Compressed content size is 0, skipping decompression"); - return true; - } - - // Check if content is actually in gzip format (gzip header magic is 0x1f 0x8b) - if (compressedSize < 2 || (uint8_t)bodyStart[0] != 0x1f || (uint8_t)bodyStart[1] != 0x8b) { - FF_DEBUG("Content is not valid gzip format, skipping decompression"); - return false; - } - - // Predict uncompressed size - uint32_t estimatedSize = guessGzipOutputSize(bodyStart, compressedSize); - - // Create decompression buffer with estimated size - FF_STRBUF_AUTO_DESTROY decompressedBuffer = ffStrbufCreateA(estimatedSize > 0 ? estimatedSize : compressedSize * 5); - FF_DEBUG("Created decompression buffer: %u bytes", decompressedBuffer.allocated); - - // Initialize decompression - z_stream zs = { - .zalloc = Z_NULL, - .zfree = Z_NULL, - .opaque = Z_NULL, - .avail_in = (uInt)compressedSize, - .next_in = (Bytef*)bodyStart, - .avail_out = (uInt)ffStrbufGetFree(&decompressedBuffer), - .next_out = (Bytef*)decompressedBuffer.chars, - }; - - // Initialize decompression engine - if (zlibData.ffinflateInit2_(&zs, 16 + MAX_WBITS, ZLIB_VERSION, (int)sizeof(z_stream)) != Z_OK) { - FF_DEBUG("Failed to initialize decompression engine"); - return false; - } - uInt availableOut = zs.avail_out; - - // Perform decompression - int result = zlibData.ffinflate(&zs, Z_FINISH); - - // If output buffer is insufficient, try to extend buffer - while (result == Z_BUF_ERROR || (result != Z_STREAM_END && zs.avail_out == 0)) - { - FF_DEBUG("Output buffer insufficient, trying to extend"); - - // Save already decompressed data amount - uint32_t alreadyDecompressed = (uint32_t)(availableOut - zs.avail_out); - decompressedBuffer.length += alreadyDecompressed; - decompressedBuffer.chars[decompressedBuffer.length] = '\0'; // Ensure null-terminated string - - ffStrbufEnsureFree(&decompressedBuffer, decompressedBuffer.length / 2); - - // Set output parameters to point to new buffer - zs.avail_out = (uInt)ffStrbufGetFree(&decompressedBuffer); - zs.next_out = (Bytef*)(decompressedBuffer.chars + decompressedBuffer.length); - availableOut = zs.avail_out; - - // Decompress again - result = zlibData.ffinflate(&zs, Z_FINISH); - } - - zlibData.ffinflateEnd(&zs); - - // Calculate decompressed size - uint32_t decompressedSize = (uint32_t)(availableOut - zs.avail_out); - decompressedBuffer.length += decompressedSize; - decompressedBuffer.chars[decompressedSize] = '\0'; // Ensure null-terminated string - FF_DEBUG("Successfully decompressed %u bytes compressed data to %u bytes", compressedSize, decompressedBuffer.length); - - // Modify Content-Length header and remove Content-Encoding header - FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerSize + decompressedSize + 64); - - char* line = NULL; - size_t len = 0; - while (ffStrbufGetline(&line, &len, buffer)) - { - if (ffStrStartsWithIgnCase(line, "Content-Encoding:")) - { - continue; - } - else if (ffStrStartsWithIgnCase(line, "Content-Length:")) - { - ffStrbufAppendF(&newBuffer, "Content-Length: %u\r\n", decompressedSize); - continue; - } - else if (line[0] == '\r') - { - ffStrbufAppendS(&newBuffer, "\r\n"); - ffStrbufGetlineRestore(&line, &len, buffer); - break; - } - - ffStrbufAppendS(&newBuffer, line); - ffStrbufAppendC(&newBuffer, '\n'); - } - - ffStrbufAppend(&newBuffer, &decompressedBuffer); - ffStrbufDestroy(buffer); - ffStrbufInitMove(buffer, &newBuffer); - - return true; -} -#endif - 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); @@ -426,7 +243,7 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho state->compression = false; #ifdef FF_HAVE_ZLIB - const char* zlibError = loadZlibLibrary(); + const char* zlibError = ffNetworkingLoadZlibLibrary(); // Only enable compression if zlib library is successfully loaded if (zlibError == NULL) { @@ -584,7 +401,7 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf #ifdef FF_HAVE_ZLIB if (state->compression) { FF_DEBUG("Content received, checking if compressed"); - if (!decompressGzip(buffer, headerEnd)) { + if (!ffNetworkingDecompressGzip(buffer, headerEnd)) { FF_DEBUG("Decompression failed or invalid compression format"); return "Failed to decompress or invalid format"; } else { diff --git a/src/common/networking_windows.c b/src/common/networking_windows.c index 305f55864..2210c8fea 100644 --- a/src/common/networking_windows.c +++ b/src/common/networking_windows.c @@ -4,37 +4,68 @@ //Must be included after #include "fastfetch.h" #include "common/networking.h" +#include "util/stringUtils.h" +#include "util/debug.h" static LPFN_CONNECTEX ConnectEx; static const char* initWsaData(WSADATA* wsaData) { - if(WSAStartup(MAKEWORD(2, 2), wsaData) != 0) + FF_DEBUG("Initializing WinSock"); + if(WSAStartup(MAKEWORD(2, 2), wsaData) != 0) { + FF_DEBUG("WSAStartup() failed"); return "WSAStartup() failed"; + } - if(LOBYTE(wsaData->wVersion) != 2 || HIBYTE(wsaData->wVersion) != 2) + if(LOBYTE(wsaData->wVersion) != 2 || HIBYTE(wsaData->wVersion) != 2) { + FF_DEBUG("Invalid wsaData version found: %d.%d", LOBYTE(wsaData->wVersion), HIBYTE(wsaData->wVersion)); return "Invalid wsaData version found"; + } //Dummy socket needed for WSAIoctl SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0); - if(sockfd == INVALID_SOCKET) + if(sockfd == INVALID_SOCKET) { + FF_DEBUG("socket(AF_INET, SOCK_STREAM) failed"); return "socket(AF_INET, SOCK_STREAM) failed"; + } DWORD dwBytes; GUID guid = WSAID_CONNECTEX; if(WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &ConnectEx, sizeof(ConnectEx), - &dwBytes, NULL, NULL) != 0) + &dwBytes, NULL, NULL) != 0) { + FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed"); return "WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed"; + } closesocket(sockfd); + FF_DEBUG("WinSock initialized successfully"); return NULL; } 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); + + // Initialize with compression disabled by default + state->compression = false; + + #ifdef FF_HAVE_ZLIB + const char* zlibError = ffNetworkingLoadZlibLibrary(); + // Only enable compression if zlib library is successfully loaded + if (zlibError == NULL) + { + state->compression = true; + FF_DEBUG("Successfully loaded zlib library, compression enabled"); + } else { + FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError); + } + #else + FF_DEBUG("zlib not supported at build time, compression disabled"); + #endif + static WSADATA wsaData; if (wsaData.wVersion == 0) { @@ -42,27 +73,54 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho if (error != NULL) { wsaData.wVersion = (WORD) -1; + FF_DEBUG("WinSock initialization failed: %s", error); return error; } } else if (wsaData.wVersion == (WORD) -1) + { + FF_DEBUG("WinSock initialization previously failed"); return "initWsaData() failed before"; + } struct addrinfo* addr; - - if(getaddrinfo(host, "80", &(struct addrinfo) { + struct addrinfo hints = { .ai_family = state->ipv6 ? AF_INET6 : AF_INET, .ai_socktype = SOCK_STREAM, - }, &addr) != 0) + .ai_flags = AI_NUMERICSERV, + }; + + FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4"); + if(getaddrinfo(host, "80", &hints, &addr) != 0) + { + FF_DEBUG("getaddrinfo() failed"); return "getaddrinfo() failed"; + } state->sockfd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); if(state->sockfd == INVALID_SOCKET) { + FF_DEBUG("socket() failed"); freeaddrinfo(addr); return "socket() failed"; } + #ifdef TCP_NODELAY + // Enable TCP_NODELAY to disable Nagle's algorithm + DWORD flag = 1; + if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_NODELAY: %d", WSAGetLastError()); + } else { + FF_DEBUG("Successfully disabled Nagle's algorithm"); + } + #endif + + // Set timeout if needed + if (state->timeout > 0) { + FF_DEBUG("Setting connection timeout: %u ms", state->timeout); + setsockopt(state->sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&state->timeout, sizeof(state->timeout)); + } + //ConnectEx requires the socket to be initially bound if((state->ipv6 ? bind(state->sockfd, (SOCKADDR *) &(struct sockaddr_in6) { @@ -74,44 +132,69 @@ const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* ho .sin_addr.s_addr = INADDR_ANY, }, sizeof(struct sockaddr_in))) != 0) { + FF_DEBUG("bind() failed: %d", WSAGetLastError()); closesocket(state->sockfd); freeaddrinfo(addr); state->sockfd = INVALID_SOCKET; return "bind() failed"; } + // Initialize overlapped structure for asynchronous I/O + memset(&state->overlapped, 0, sizeof(OVERLAPPED)); + + // Build HTTP command FF_STRBUF_AUTO_DESTROY command = ffStrbufCreateA(64); ffStrbufAppendS(&command, "GET "); ffStrbufAppendS(&command, path); ffStrbufAppendS(&command, " HTTP/1.1\nHost: "); ffStrbufAppendS(&command, host); ffStrbufAppendS(&command, "\r\n"); + ffStrbufAppendS(&command, "Connection: close\r\n"); // Explicitly request connection closure + + // Add compression support if enabled + if (state->compression) { + FF_DEBUG("Enabling HTTP content compression"); + ffStrbufAppendS(&command, "Accept-Encoding: gzip\r\n"); + } + ffStrbufAppendS(&command, headers); ffStrbufAppendS(&command, "\r\n"); - BOOL result = ConnectEx(state->sockfd, addr->ai_addr, (int)addr->ai_addrlen, command.chars, command.length, NULL, &state->overlapped); + FF_DEBUG("Using ConnectEx to send %u bytes of data", command.length); + BOOL result = ConnectEx(state->sockfd, addr->ai_addr, (int)addr->ai_addrlen, + command.chars, command.length, NULL, &state->overlapped); + freeaddrinfo(addr); if(!result && WSAGetLastError() != WSA_IO_PENDING) { + FF_DEBUG("ConnectEx() failed: %d", WSAGetLastError()); closesocket(state->sockfd); state->sockfd = INVALID_SOCKET; return "ConnectEx() failed"; } + // No need to cleanup state fields here since we need them in the receive function return NULL; } const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) { + FF_DEBUG("Preparing to receive HTTP response"); + if (state->sockfd == INVALID_SOCKET) + { + FF_DEBUG("Invalid socket, HTTP request might have failed"); return "ffNetworkingSendHttpRequest() failed"; + } uint32_t timeout = state->timeout; if (timeout > 0) { - if (WaitForSingleObject((HANDLE) state->sockfd, timeout) != WAIT_OBJECT_0) + FF_DEBUG("WaitForSingleObject with timeout: %u ms", timeout); + if (WaitForSingleObject((HANDLE)state->sockfd, timeout) != WAIT_OBJECT_0) { + FF_DEBUG("WaitForSingleObject failed or timed out"); CancelIo((HANDLE) state->sockfd); closesocket(state->sockfd); return "WaitForSingleObject(state->sockfd) failed or timeout"; @@ -121,26 +204,102 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf DWORD transfer, flags; if (!WSAGetOverlappedResult(state->sockfd, &state->overlapped, &transfer, TRUE, &flags)) { + FF_DEBUG("WSAGetOverlappedResult failed: %d", WSAGetLastError()); closesocket(state->sockfd); return "WSAGetOverlappedResult() failed"; } if(timeout > 0) { - //https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-setsockopt - setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*) &timeout, sizeof(timeout)); + FF_DEBUG("Setting receive timeout: %u ms", timeout); + setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); } - uint32_t recvStart; - do { - recvStart = buffer->length; - ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, (int) ffStrbufGetFree(buffer), 0); - if (received <= 0) break; - buffer->length = recvStart + (uint32_t) received; - buffer->chars[buffer->length] = '\0'; - } while (ffStrbufGetFree(buffer) > 0 && strstr(buffer->chars + recvStart, "\r\n\r\n") == NULL); + // Set larger receive buffer for better performance + int rcvbuf = 65536; // 64KB + setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, (const char*)&rcvbuf, sizeof(rcvbuf)); + FF_DEBUG("Starting data reception"); + FF_MAYBE_UNUSED int recvCount = 0; + uint32_t contentLength = 0; + char* headerEnd = NULL; + + do { + size_t availSpace = ffStrbufGetFree(buffer); + FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %zu", + ++recvCount, buffer->length, availSpace); + + ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, (int)ffStrbufGetFree(buffer), 0); + + if (received <= 0) { + if (received == 0) { + FF_DEBUG("Connection closed (received=0)"); + } else { + FF_DEBUG("Reception failed: %d", WSAGetLastError()); + } + 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 == NULL) { + headerEnd = strstr(buffer->chars, "\r\n\r\n"); + if (headerEnd != NULL) { + FF_DEBUG("Found HTTP header end marker, position: %ld", (long)(headerEnd - buffer->chars)); + + // 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 + 16, 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=%u", (unsigned)state->sockfd); closesocket(state->sockfd); - if (buffer->length == 0) return "Empty server response received"; - return ffStrbufStartsWithS(buffer, "HTTP/1.1 200 OK\r\n") ? NULL : "Invalid response"; + state->sockfd = INVALID_SOCKET; + + if (buffer->length == 0) { + FF_DEBUG("Server response is empty"); + return "Empty server response received"; + } + + if (headerEnd == NULL) { + FF_DEBUG("No HTTP header end marker found"); + return "No HTTP header end found"; + } + + if (ffStrbufStartsWithS(buffer, "HTTP/1.1 200 OK\r\n")) { + FF_DEBUG("Received valid HTTP 200 response, content length: %u bytes, total length: %u bytes", + contentLength, buffer->length); + } else { + FF_DEBUG("Invalid response: %.40s...", buffer->chars); + return "Invalid response"; + } + + // If compression was used, try to decompress + #ifdef FF_HAVE_ZLIB + if (state->compression) { + FF_DEBUG("Content received, checking if compressed"); + if (!ffNetworkingDecompressGzip(buffer, 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; } diff --git a/src/util/debug.h b/src/util/debug.h index 9e9d2e710..70a990a68 100644 --- a/src/util/debug.h +++ b/src/util/debug.h @@ -2,13 +2,11 @@ static inline const char* ffFindFileName(const char* file) { - const char* lastSlash = __builtin_strrchr(file, - #ifdef _WIN32 - '\\' - #else - '/' - #endif - ); + const char* lastSlash = __builtin_strrchr(file, '/'); + #ifdef _WIN32 + if (lastSlash == NULL) + lastSlash = __builtin_strrchr(file, '\\'); + #endif if (lastSlash != NULL) return lastSlash + 1; return file;