AzureSphereSquirrel/HLCore/http.cpp (383 lines of code) (raw):
/* Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. */
#include "http.h"
#include "squirrel_cpp_helper.h"
#include <stdlib.h>
#include <string.h>
#include "squirrel/include/sqstdblob.h"
#include "http_request.h"
#include "curl_logs.h"
#include <applibs/application.h>
#include <applibs/storage.h>
#include <tlsutils/deviceauth.h>
#include <tlsutils/deviceauth_curl.h> // required only for mutual authentication
#include <applibs/networking_curl.h> // required only if using proxy to connect to the internet
extern "C"
{
#include <wolfssl/ssl.h>
}
// Static Methods
//---------------
/// Creates and registers an instance of the HTTP Class in the Squirrel roottable as a globally available object
/// and exposes methods so they may be called from within the VM.
/// \param vm the instance of the VM to use.
/// \returns the registered instance.
HTTP* HTTP::registerWithSquirrelAsGlobal(HSQUIRRELVM vm, EventLoop *eventLoop, const char* name)
{
SquirrelCppHelper::DelegateFunction delegateFunctions[5];
delegateFunctions[0] = SquirrelCppHelper::DelegateFunction("get", &HTTP::SQUIRREL_METHOD_NAME(get));
delegateFunctions[1] = SquirrelCppHelper::DelegateFunction("put", &HTTP::SQUIRREL_METHOD_NAME(put));
delegateFunctions[2] = SquirrelCppHelper::DelegateFunction("post", &HTTP::SQUIRREL_METHOD_NAME(post));
delegateFunctions[3] = SquirrelCppHelper::DelegateFunction("request", &HTTP::SQUIRREL_METHOD_NAME(request));
delegateFunctions[4] = SquirrelCppHelper::DelegateFunction("getDeviceIdFromDAA", &HTTP::SQUIRREL_METHOD_NAME(getDeviceIdFromDAA));
HTTP* http = SquirrelCppHelper::registerClassAsGlobal<HTTP>(vm, name, delegateFunctions, 5);
http->initialise(vm, eventLoop);
return http;
}
/// Provides a CURLMOPT_TIMERFUNCTION compatible callback, forwarding timer callbacks to the apropriate HTTP instance.
int HTTP::curlTimerCallback(CURLM *multiHandle, long timeout_ms, void *httpUserData)
{
return ((HTTP*)httpUserData)->_curlTimerCallback(multiHandle, timeout_ms);
}
/// Provides an EventLoopTimer compatible callback, forwarding timer events to the apropriate HTTP instance.
void HTTP::curlTimerEventHandler(EventLoopTimer *timer)
{
((HTTP*)timer->context)->_curlTimerEventHandler(timer);
}
/// Provides a CURLMOPT_SOCKETFUNCTION compatible callback, forwarding socket events to the apropriate HTTP instance.
int HTTP::curlMSocketCallback(CURL *easyHandle, curl_socket_t socketFd, int action, void *httpUserData, void *socketUserData)
{
return ((HTTP*)httpUserData)->_curlMSocketCallback(easyHandle, socketFd, action, socketUserData);
}
/// Provides an EventLoop I/O compatible callback, forwarding file I/O events to the apropriate HTTP instance.
void HTTP::curlFdEventHandler(EventLoop *eventLoop, int socketFd, EventLoop_IoEvents events, void *httpUserData)
{
((HTTP*)httpUserData)->_curlFdEventHandler(eventLoop, socketFd, events);
}
/// Provides a CURLOPT_READFUNCTION compatible callback, forwarding read requests to the apropriate HTTPRequest.
size_t HTTP::curlReadCallback(void *buffer, size_t size, size_t nitems, void *userdata)
{
return ((HTTPRequest*)userdata)->curlReadCallback(buffer, size * nitems);
}
/// Provides a CURLOPT_WRITEFUNCTION compatible callback, forwarding read requests to the apropriate HTTPRequest.
size_t HTTP::curlWriteCallback(void *data, size_t size, size_t nmemb, void *userp)
{
return ((HTTPRequest*)userp)->curlWriteCallback(data, nmemb);
}
/// Provides a CURLOPT_HEADERFUNCTION compatible callback, forwarding read requests to the apropriate HTTPRequest.
size_t HTTP::curlWriteHeaderCallback(void *buffer, size_t size, size_t nitems, void *userdata)
{
return ((HTTPRequest*)userdata)->curlWriteHeaderCallback(buffer, nitems);
}
/*
/// Helpful function for debug only.
int HTTP::curlDebugCallback(CURL *handle, curl_infotype type, char *data, size_t size, void *userptr)
{
const char *text;
(void)handle; // prevent compiler warning
(void)userptr;
switch (type) {
case CURLINFO_TEXT:
Log_Debug("== Info: %s", data);
default: // in case a new one is introduced to shock us
return 0;
case CURLINFO_HEADER_OUT:
text = "=> Send header";
break;
case CURLINFO_DATA_OUT:
text = "=> Send data";
break;
case CURLINFO_SSL_DATA_OUT:
text = "=> Send SSL data";
break;
case CURLINFO_HEADER_IN:
text = "<= Recv header";
break;
case CURLINFO_DATA_IN:
text = "<= Recv data";
break;
case CURLINFO_SSL_DATA_IN:
text = "<= Recv SSL data";
break;
}
Log_Debug(text);
}*/
// Squirrel Methods
//-----------------
/// Constructs a GET HTTPRequest and places it on the stack.
/// \param vm the instance of the VM to use.
/// \returns SQ_SUCCEEDED on success, otherwise SQ_FAILED.
/// \throws throws within the Squirrel VM if it's not possible to retrieve user pointers to the instance or method.
SQUIRREL_METHOD_IMPL(HTTP, get)
{
int types[] = {OT_STRING, OT_TABLE};
if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 2, 2, types)))
{
return SQ_ERROR;
}
// Fetch the method parameters
const SQChar *url;
sq_getstring(vm, 2, &url);
HSQOBJECT headers;
sq_getstackobj(vm, 3, &headers);
// Convert the headers table to a curl string linked list by iterating over the table
curl_slist *headerList;
SQInteger result = generateHeaderList(vm, headers, headerList);
if(SQ_FAILED(result))
{
return result;
}
// Create, push to stack and setup the new custom HTTPRequest
return HTTPRequest::newHTTPRequest(vm, curlMulti, curlTemplate, "GET", url, headerList);
}
/// Constructs a PUT HTTPRequest and places it on the stack.
/// \param vm the instance of the VM to use.
/// \returns SQ_SUCCEEDED on success, otherwise SQ_FAILED.
/// \throws throws within the Squirrel VM if it's not possible to retrieve user pointers to the instance or method.
SQUIRREL_METHOD_IMPL(HTTP, put)
{
int types[] = {OT_STRING, OT_TABLE, OT_STRING | OT_INSTANCE};
if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 3, 3, types)))
{
return SQ_ERROR;
}
// Fetch the method parameters
const SQChar *url;
sq_getstring(vm, 2, &url);
HSQOBJECT headers;
sq_getstackobj(vm, 3, &headers);
SQChar *body;
SQInteger bodySize;
SQInteger result = retrieveAndCopyBody(vm, 4, body, bodySize);
if(SQ_FAILED(result))
{
return result;
}
// Convert the headers table to a curl string linked list by iterating over the table
curl_slist *headerList;
result = generateHeaderList(vm, headers, headerList);
if(SQ_FAILED(result))
{
sq_free(body, bodySize);
return result;
}
// Create, push to stack and setup the new custom HTTPRequest
return HTTPRequest::newHTTPRequest(vm, curlMulti, curlTemplate, "PUT", url, headerList, body, bodySize);
}
/// Constructs a PUT HTTPRequest and places it on the stack.
/// \param vm the instance of the VM to use.
/// \returns SQ_SUCCEEDED on success, otherwise SQ_FAILED.
/// \throws throws within the Squirrel VM if it's not possible to retrieve user pointers to the instance or method.
SQUIRREL_METHOD_IMPL(HTTP, post)
{
int types[] = {OT_STRING, OT_TABLE, OT_STRING | OT_INSTANCE};
if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 3, 3, types)))
{
return SQ_ERROR;
}
// Fetch the method parameters
const SQChar *url;
sq_getstring(vm, 2, &url);
HSQOBJECT headers;
sq_getstackobj(vm, 3, &headers);
SQChar *body;
SQInteger bodySize;
SQInteger result = retrieveAndCopyBody(vm, 4, body, bodySize);
if(SQ_FAILED(result))
{
return result;
}
// Convert the headers table to a curl string linked list by iterating over the table
curl_slist *headerList;
result = generateHeaderList(vm, headers, headerList);
if(SQ_FAILED(result))
{
sq_free(body, bodySize);
return result;
}
// Create, push to stack and setup the new custom HTTPRequest
return HTTPRequest::newHTTPRequest(vm, curlMulti, curlTemplate, "POST", url, headerList, body, bodySize);
}
/// Constructs a generic HTTPRequest and places it on the stack.
/// \param vm the instance of the VM to use.
/// \returns SQ_SUCCEEDED on success, otherwise SQ_FAILED.
/// \throws throws within the Squirrel VM if it's not possible to retrieve user pointers to the instance or method.
SQUIRREL_METHOD_IMPL(HTTP, request)
{
int types[] = {OT_STRING, OT_STRING, OT_TABLE, OT_STRING | OT_INSTANCE};
if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 4, 4, types)))
{
return SQ_ERROR;
}
// Fetch the method parameters
const SQChar *verb;
sq_getstring(vm, 2, &verb);
const SQChar *url;
sq_getstring(vm, 3, &url);
HSQOBJECT headers;
sq_getstackobj(vm, 4, &headers);
SQChar *body;
SQInteger bodySize;
SQInteger result = retrieveAndCopyBody(vm, 5, body, bodySize);
if(SQ_FAILED(result))
{
return result;
}
// Convert the headers table to a curl string linked list by iterating over the table
curl_slist *headerList;
result = generateHeaderList(vm, headers, headerList);
if(SQ_FAILED(result))
{
sq_free(body, bodySize);
return result;
}
// Create, push to stack and setup the new custom HTTPRequest
return HTTPRequest::newHTTPRequest(vm, curlMulti, curlTemplate, verb, url, headerList, body, bodySize);
}
/// Retrieves the device ID from the DAA certificate.
/// \param vm the instance of the VM to use.
/// \returns SQ_SUCCEEDED on success, otherwise SQ_FAILED.
/// \throws throws within the Squirrel VM if the DAA certificate has not yet been obtained or WolfSSL fails.
SQUIRREL_METHOD_IMPL(HTTP, getDeviceIdFromDAA)
{
int types[] = {OT_NULL};
if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 0, 0, types)))
{
return SQ_ERROR;
}
// Ensure that we have received the DAA certificate
bool daaRetrieved = false;
if(Application_IsDeviceAuthReady(&daaRetrieved) < 0 || !daaRetrieved)
{
return sq_throwerror(vm, "The DAA Certification needed to retrieve Device ID has not yet been obtained");
}
// Initialise WolfSSL so that we can retrieve the DAA certificate
if(wolfSSL_Init() != WOLFSSL_SUCCESS)
{
return sq_throwerror(vm, "Unable to initialise WolfSSL");
}
// Retrieve the DAA certificate
WOLFSSL_X509 *daaCertificate = wolfSSL_X509_load_certificate_file(DeviceAuth_GetCertificatePath(), WOLFSSL_FILETYPE_PEM);
if(daaCertificate == NULL)
{
wolfSSL_Cleanup();
return sq_throwerror(vm, "WolfSSL was unable to load the DAA certifcate file");
}
// Extract the DAA certifcate subject name (this contains the device ID)
WOLFSSL_X509_NAME* subjectName = wolfSSL_X509_get_subject_name(daaCertificate);
if(subjectName == NULL)
{
wolfSSL_X509_free(daaCertificate);
wolfSSL_Cleanup();
return sq_throwerror(vm, "WolfSSL was unable to extract the subject name from the DAA certifcate");
}
// Extract the device ID from the subject and place on the stack as a return value
char deviceId[134] = { 0 };
if(wolfSSL_X509_NAME_oneline(subjectName, (char*)&deviceId, sizeof(deviceId)) < 0)
{
wolfSSL_X509_free(daaCertificate);
wolfSSL_Cleanup();
return sq_throwerror(vm, "WolfSSL was unable to extract the device id from the subject in the DAA certifcate");
}
// Cleanup WolfSSL
wolfSSL_X509_free(daaCertificate);
wolfSSL_Cleanup();
sq_pushstring(vm, deviceId+4, sizeof(deviceId)-6);
// Return the Squirrel object from the top of the stack
return 1;
}
// Methods
//--------
/// Retrieves a HTTP request body (string|blob) from the specified stack location and provides a copy of the underlying data.
/// \note Ownership of outputBody is passed to the function caller and must be freed once no longer needed.
/// \param vm the instance of the VM to use.
/// \param stackLocation the location on the stack where the body string|blob may be found.
/// \param outputBody will be populated with a heap allocated copy of the underlying data from the body string|blob (must be freed once no longer needed).
/// \param outputBodySize will be populated with the size of the data pointed to by outputBody.
/// \returns SQ_SUCCEEDED on success, otherwise SQ_FAILED.
/// \throws throws within the Squirrel VM and so return value should be passed out of the calling function back to the VM.
SQInteger HTTP::retrieveAndCopyBody(HSQUIRRELVM vm, SQInteger stackLocation, SQChar *&outputBody, SQInteger &outputBodySize)
{
const SQChar *body;
if(sq_gettype(vm, stackLocation) == OT_STRING)
{
sq_getstringandsize(vm, stackLocation, (const SQChar**)&body, &outputBodySize);
outputBody = (SQChar*)sq_malloc(outputBodySize);
memcpy((void*)outputBody, (void*)body, outputBodySize);
}
else
{
if(SQ_FAILED(sqstd_getblob(vm, stackLocation, (SQUserPointer*)&outputBody)))
{
return SQUIRREL_THROW_BAD_PARAMETER_TYPE(vm);
}
outputBodySize = sqstd_getblobsize(vm, stackLocation);
outputBody = (SQChar*)sq_malloc(outputBodySize);
memcpy((void*)outputBody, (void*)body, outputBodySize);
}
return 0;
}
/// Generates a curl compliant HTTP header string linked list from the specified header table object.
/// \note Ownership of outputHeaderList is passed to the function caller and must be freed once no longer needed.
/// \param vm the instance of the VM to use.
/// \param headers the table object to extract HTTP headers from.
/// \param outputHeaderList will be populated with a heap allocated copy of the headers converted into a curl_slist linked list (must be freed once no longer needed).
/// \returns SQ_SUCCEEDED on success, otherwise SQ_FAILED.
/// \throws throws within the Squirrel VM and so return value should be passed out of the calling function back to the VM.
SQInteger HTTP::generateHeaderList(HSQUIRRELVM vm, HSQOBJECT headers, curl_slist *&outputHeaderList)
{
// Convert the headers table to a curl string linked list by iterating over the table
outputHeaderList = nullptr;
sq_pushobject(vm, headers);
sq_pushnull(vm);
SQChar headerString[1024];
while(SQ_SUCCEEDED(sq_next(vm, -2)))
{
const SQChar *key;
SQInteger keyLen;
sq_getstringandsize(vm, -2, &key, &keyLen);
const SQChar *val;
SQInteger valLen;
sq_getstringandsize(vm, -1, &val, &valLen);
if( keyLen + valLen + 2 > 1024 )
{
curl_slist_free_all(outputHeaderList);
return sq_throwerror(vm, "At least one header exceeds the maximum allowable size of 1KB.");
}
strncpy( headerString, key, keyLen );
strcat( headerString, ": " );
strncat( headerString, val, valLen );
outputHeaderList = curl_slist_append(outputHeaderList, headerString);
sq_pop(vm, 2);
}
return 0;
}
/// Triggers CURL to process the next stage of transfers on the associated CurlMulti handle.
void HTTP::curlProcessTransfers()
{
CURLMcode code = curl_multi_socket_action(curlMulti, CURL_SOCKET_TIMEOUT, 0, &activeEasyHandles);
if(code != CURLM_OK)
{
LogCurlMultiError("curl_multi_socket_action", code);
}
}
/// Handles Curl timer callbacks, resecheduling the timeout timer with the latest timeout provided.
/// \param multiHandle a pointer to a Curl Multi handle.
/// \param timeout_ms the timeout (in milliseconds) to configure the timer for.
/// \returns '0' on Success, otherwise '<0'.
int HTTP::_curlTimerCallback(CURLM *multiHandle, long timeout_ms)
{
// A value of -1 means the timer does not need to be started.
if(timeout_ms != -1)
{
// Invoke cURL immediately if requested to do so.
if(timeout_ms == 0)
{
curlProcessTransfers();
}
else
{
// Start a single shot timer with the period as provided by cURL.
// The timer handler will invoke cURL to process the web transfers.
const struct timespec timeout = {.tv_sec = timeout_ms / 1000,
.tv_nsec = (timeout_ms % 1000) * 1000000};
SetEventLoopTimerOneShot(curlTimeoutTimer, &timeout);
}
}
return 0;
}
/// Handles the timeout timer triggered event by consuming the event and prompting
/// curl to process the next stage of transfers.
/// \param timer a pointer to the timer object that triggered the event.
void HTTP::_curlTimerEventHandler(EventLoopTimer *timer)
{
if(ConsumeEventLoopTimerEvent(timer) != 0)
{
Log_Debug("ERROR: cannot consume the timer event.\n");
return;
}
curlProcessTransfers();
}
/// Handles Curl socket lifetime events by adding, modifying and removing them for event monitoring.
/// \param easyHandle a pointer to the Curl Easy handle of the socket.
/// \param socketFd the file descriptor of the socket (with which to listen or not for events).
/// \param action the action to take (add/modify/remove from event monitoring).
/// \param socketUserData a reference to the I/O event currently being monitored (otherwise NULL).
/// \returns '0' on Success, otherwise '<0'.
int HTTP::_curlMSocketCallback(CURL *easyHandle, curl_socket_t socketFd, int action, void *socketUserData)
{
EventRegistration *socketEvent = (EventRegistration *)socketUserData;
// Determine if we should unregister sockets for monitoring by the event loop
if(action == CURL_POLL_REMOVE)
{
int result = EventLoop_UnregisterIo(eventLoop, socketEvent);
// Note: Allow EBADF errors as sometimes the kernel has done this cleanup for us already
if (result == -1 && errno != EBADF)
{
Log_Debug("ERROR: Cannot unregister IO event: %d\n", errno);
}
return CURLM_OK;
}
// If we're not currently monitoring the socketFd, do so
if(socketEvent == NULL)
{
socketEvent = EventLoop_RegisterIo(eventLoop, socketFd, 0x0, HTTP::curlFdEventHandler, this);
if(socketEvent == NULL)
{
LogErrno("ERROR: Could not create socket event", errno);
return -1;
}
curl_multi_assign(curlMulti, socketFd, socketEvent);
}
// Determine if we should be monitoring for in, out or both events and update the events mask
EventLoop_IoEvents eventsMask = 0;
if(action == CURL_POLL_IN || action == CURL_POLL_INOUT)
{
eventsMask |= EventLoop_Input;
}
if(action == CURL_POLL_OUT || action == CURL_POLL_INOUT)
{
eventsMask |= EventLoop_Output;
}
int result = EventLoop_ModifyIoEvents(eventLoop, socketEvent, eventsMask);
if(result == -1)
{
LogErrno("ERROR: Could not add or modify socket event mask", socketFd);
return -1;
}
return CURLM_OK;
}
/// Handles file I/O events, requesting Curl to read/write data etc.
/// \param eventLoop a pointer to the event loop that picked-up the event (UNUSED).
/// \param socketFd the file descriptor of the socket on which the event occured.
/// \param events a bitmask of which events occured on the socket (UNUSED).
void HTTP::_curlFdEventHandler(EventLoop *eventLoop, int socketFd, EventLoop_IoEvents events)
{
CURLMcode code;
int newActiveEasyHandles = 0;
if((code = curl_multi_socket_action(curlMulti, socketFd, 0, &newActiveEasyHandles)) != CURLM_OK)
{
LogCurlMultiError("curl_multi_socket_action", code);
return;
}
// Each time the 'running_handles' counter changes, curl_multi_info_read() will return info
// about the specific transfers that completed.
if(newActiveEasyHandles != activeEasyHandles)
{
int numberOfMessagesInQueue;
CURLMsg *curlMessage;
while((curlMessage = curl_multi_info_read(curlMulti, &numberOfMessagesInQueue)) != NULL)
{
if(curlMessage->msg == CURLMSG_DONE)
{
HTTPRequest *httpRequest;
curl_easy_getinfo(curlMessage->easy_handle, CURLINFO_PRIVATE, (void**)&httpRequest);
httpRequest->processResult(curlMessage->data.result);
}
}
}
activeEasyHandles = newActiveEasyHandles;
}
/// Initialises the instance, this should be a constructor but without try/throw that wasn't possible.
/// @param vm the VM instance upon which to act (UNUSED).
/// @param eventLoop a pointer to the EventLoop to register async timers/events with.
/// \return '0' on Success, otherwise '<0'.
int HTTP::initialise(HSQUIRRELVM vm, EventLoop *eventLoop)
{
this->eventLoop = eventLoop;
// Initialise CURL
curl_global_init(CURL_GLOBAL_ALL);
// Create a CURL easy handle to hold system-wide configuration, for use as a template for other handles
curlTemplate = curl_easy_init();
CURLcode result;
// Enable redirect following
result = curl_easy_setopt(curlTemplate, CURLOPT_FOLLOWLOCATION, 1L);
// Restrict requests to HTTP(S)
long allowedProtocols = CURLPROTO_HTTP | CURLPROTO_HTTPS;
result = curl_easy_setopt(curlTemplate, CURLOPT_PROTOCOLS, allowedProtocols);
// Restrict redirects to HTTP(S)
result = curl_easy_setopt(curlTemplate, CURLOPT_REDIR_PROTOCOLS, allowedProtocols);
// Load the HTTPS certificate(s)
char *certificatePath = Storage_GetAbsolutePathInImagePackage("certs/CA.cer");
result = curl_easy_setopt(curlTemplate, CURLOPT_CAINFO, certificatePath);
// Enable mTLS
result = curl_easy_setopt(curlTemplate, CURLOPT_SSL_CTX_FUNCTION, DeviceAuth_CurlSslFunc);
// Turn of verbose messaging (for performance/space)
result = curl_easy_setopt(curlTemplate, CURLOPT_VERBOSE, 0);
// Use proxy
//Networking_Curl_SetDefaultProxy(curlTemplate);
// Send headers to the server only and not the proxy (needed for correct HTTPS proxy support)
result = curl_easy_setopt(curlTemplate, CURLOPT_HEADEROPT, CURLHEADER_SEPARATE);
// Attach function to handle downloaded data
result = curl_easy_setopt(curlTemplate, CURLOPT_WRITEFUNCTION, HTTP::curlWriteCallback);
// Attach function to handle downloaded headers
result = curl_easy_setopt(curlTemplate, CURLOPT_HEADERFUNCTION, HTTP::curlWriteHeaderCallback);
// Attach function to handle uploaded data
result = curl_easy_setopt(curlTemplate, CURLOPT_READFUNCTION, HTTP::curlReadCallback);
// Create a CURL Multi handle for high-performance aysnc requests
curlMulti = curl_multi_init();
CURLMcode multiResult;
multiResult = curl_multi_setopt(curlMulti, CURLMOPT_SOCKETFUNCTION, HTTP::curlMSocketCallback);
multiResult = curl_multi_setopt(curlMulti, CURLMOPT_SOCKETDATA, this);
multiResult = curl_multi_setopt(curlMulti, CURLMOPT_TIMERFUNCTION, HTTP::curlTimerCallback);
multiResult = curl_multi_setopt(curlMulti, CURLMOPT_TIMERDATA, this);
activeEasyHandles = 0;
// Create an EventLoop timer to be set by CURL Multi for handling timeouts
curlTimeoutTimer = CreateEventLoopDisarmedTimer(eventLoop, HTTP::curlTimerEventHandler, this);
if(curlTimeoutTimer == NULL)
{
return -1;
}
return 0;
}
HTTP::~HTTP()
{
// Individual easy handles should have been cleaned-up by the destruction of HTTPRequest objects/VM.
// Cleanup the template CurlEasy handle
curl_easy_cleanup(curlTemplate);
// Cleanup the CurlMulti handle
curl_multi_cleanup(curlMulti);
// Cleanup Curl globally
curl_global_cleanup();
}