基于libcurl的 C/C++应用在发起HTTP POST请求时返回错误的响应
我使用以下这段代码从服务器下载数据:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include "../persistent/settings.h"
#include "network-helper.h"
#define HTTP_REQUEST_MAX_LENGTH 2048
struct MemoryStruct {
char *memory;
size_t size;
};
static size_t writeCb(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
struct MemoryStruct *mem = (struct MemoryStruct *)userp;
char *ptr = (char *)realloc(mem->memory, mem->size + realsize + 1);
if(!ptr)
{
/* out of memory! */
printf("not enough memory (realloc returned NULL)\n");
return 0;
}
mem->memory = ptr;
memcpy(&(mem->memory[mem->size]), contents, realsize);
mem->size += realsize;
mem->memory[mem->size] = 0;
printf("Curl callback:\r\n")
for (size_t i = 0; i < realsize; i++)
{
printf("%02X", contents[i]);
}
printf("\r\n");
return realsize;
}
int16_t httpPostRequest(char *endpoint, char * additionalFormData, uint8_t * response, uint16_t maxResponseLength)
{
CURL *curl;
CURLcode res;
size_t responseLength = 0;
struct MemoryStruct chunk;
static char url[256];
static char request[HTTP_REQUEST_MAX_LENGTH];
memset(url, 0x00, sizeof(url));
// serverUrls.main, authId, token are elsewhere
sprintf(url, "https://%s%s", serverUrls.main, endpoint);
memset(request, 0x00, sizeof(request));
sprintf(request, "authId=%s&token=%s%s", authId, token, additionalFormData);
printf("httpPostRequest, url = %s, request = %s\r\n", url, request);
res = curl_global_init(CURL_GLOBAL_ALL);
if(res)
{
printf("failed to initialize libcurl\r\n");
return (int)res;
}
chunk.memory = (char *)malloc(1); /* grown as needed by realloc above */
chunk.size = 0; /* no data at this point */
curl = curl_easy_init();
if(curl)
{
curl_easy_setopt(curl, CURLOPT_URL, url);
/* send all data to this function */
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCb);
/* we pass our 'chunk' struct to the callback function */
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);
/* some servers do not like requests that are made without a user-agent
field, so we provide one */
curl_easy_setopt(curl, CURLOPT_USERAGENT, "RPI-HTTP-Client/Curl");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request);
/* if we do not provide POSTFIELDSIZE, libcurl calls strlen() by itself */
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(request));
/* Perform the request, res gets the return code */
res = curl_easy_perform(curl);
/* Check for errors */
if(res != CURLE_OK)
{
printf("curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
else
{
responseLength = chunk.size;
memcpy(response, chunk.memory, chunk.size < maxResponseLength ? chunk.size : maxResponseLength);
}
/* always cleanup */
curl_easy_cleanup(curl);
}
free(chunk.memory);
curl_global_cleanup();
return responseLength;
}
这样的代码在Ubuntu 24.04上编译时似乎按预期工作。但是在树莓派上编译时,当下载的数据大于485字节时会产生错误的结果(可能更大,但测试过的最大有效长度是485)。
以下是一些示例结果:
// print out inside of writeCb, both length and content are the same as expected
Curl callback, chunksize = 787
13037C43198101000100313630692D434C2D23310000000000000000000000000116000202016810CC109411D80E0CFE640032005802B40008071815403801EC76D56900681000000000000000000000A977D5690000021600070700FF002C012C013200CEFF000001005802B40008071815403801EC76D56900FF0000000000000000000000A977D569000101280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000102280013130150005A00E80314000000050003002C01780008071815080701ED76D56900500000000000000000000000A977D5690001032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000201280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000202280013130133004600E80314000000050003002C013C0008071815080701ED76D56900330000000000000000000000A977D5690002032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000301280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000302280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690003032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000401280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900
// print out of response after calling httpPostRequest function
72019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900F6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000102280013130150005A00E80314000000050003002C01780008071815080701ED76D56900500000000000000000000000A977D5690001032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000201280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000202280013130133004600E80314000000050003002C013C0008071815080701ED76D56900330000000000000000000000A977D5690002032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000301280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000302280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690003032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000401280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900
当我比较这两次打印输出时,在调用 int16_t httpPostRequest(char *endpoint, char * additionalFormData, uint8_t * response, uint16_t maxResponseLength) 之后的响应打印中,结尾的155B数据块出现了两次,一次在它们应在的位置的末尾,一次覆盖了数据的起始部分。
72019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900
一种可能的原因是应用程序先下载了一个632B的数据块,然后再下载一个155B的数据块,这很可能是代码某处的一个错误;这个后续的数据块被复制了两次,一次复制后又覆盖了数据的起始部分。
但 printf("Curl callback:\r\n") 在 writeCb 内部的每次对 httpPostRequest 调用中只调用一次。这似乎表明 writeCb 也只被调用一次,应用程序一次性下载了整个787B。
我有点困惑,这到底是我的一个bug,还是某处存在未定义行为?
解决方案
正如 [John Bollinger] 所建议的,真正的问题并不在 httpPostRequest 和 curl 之外。curl获取的响应随后被复制到一个新结构中:
DownloadedObject downloadedObject;
memcpy((void *)&downloadedObject, response, responseLength);
此后,response 的内容发生了变化,结尾的155B数据块被复制到了开头。
我忽略了 sizeof(DownloadedObject) = 571 有时可能小于 responseLength(在本例中为787)的可能性。这在桌面上运行Ubuntu 24.04时不会造成问题,但在RAM有限的树莓派上或某些与编译器相关的未知行为中就会表现出来。
问题通过调整 struct DownloadedObject 的定义以容纳从服务器下载的最大可能大小来修复,并添加了如下的保护措施:
memcpy((void *)&downloadedObject, response,\
responseLength < sizeof(DownloadedObject) ? \
responseLength : sizeof(DownloadedObject));
我本应先尝试构造一个最小可复现的测试代码,以免浪费大家的时间。抱歉。
备选方案
httpPostRequest 在拷贝长度被 chunk.size < maxResponseLength ? chunk.size : maxResponseLength 限制时返回的长度是错误的。在这种情况下它应该返回 maxResponseLength。你可以把代码简化为:
responseLength = chunk.size < maxResponseLength ? chunk.size : maxResponseLength;
memcpy(response, chunk.memory, responseLength);
请注意,这只是解决了缓冲区溢出的问题。它会导致返回的响应被截断,如果响应中的额外字节里包含重要信息,可能会成为问题。