AzureSphereSquirrel/HLCore/http_request.cpp (241 lines of code) (raw):

/* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. */ #include "http_request.h" #include <string.h> #include <ctype.h> #include <applibs/storage.h>// temp for workaround #include <applibs/log.h>// temp for workaround #include <tlsutils/deviceauth_curl.h> #include "http.h"// temp for workaround // Static Methods //--------------- /// Creates and configures new HTTPRequest and places it upon the stack. /// \param vm the VM in which to create the HTTPRequest object. /// \param curlMulti a reference to a CurlMulti handle to use in async requests. /// \param curlTemplate a template curl request, providing base/shared parameters for duplication. /// \param verb the verb of the HTTP request e.g. "GET". /// \param url the url of the HTTP request e.g. https://www.microsoft.com/ /// \param headers a Curl-compatible linked-list of HTTP headers to be sent as part of the request (must be created with curl_slist_append, HTTPRequest will take ownership and call curl_slist_free_all). /// \param body the data body to be sent as part of the request (must be created with sq_malloc, HTTPRequest will take ownership and call sq_free). /// \param bodySize the size of the data body. SQInteger HTTPRequest::newHTTPRequest(HSQUIRRELVM vm, CURL *curlMulti, CURL *curlTemplate, const SQChar* verb, const SQChar* url, curl_slist *headers, SQChar *body, SQInteger bodySize) { // Create a new HTTPRequest instance and place it on the stack HTTPRequest *request = SquirrelCppHelper::createInstanceOnStackNoConstructor<HTTPRequest>(vm); // Assign (create if required) the delegate table to expose functionality in Squirrel if(SquirrelCppHelper::assignDelegateFromRegistry(vm, "HTTPRequest") < 0) { SquirrelCppHelper::DelegateFunction delegateFunctions[4]; delegateFunctions[0] = SquirrelCppHelper::DelegateFunction("cancel", &HTTPRequest::SQUIRREL_METHOD_NAME(cancel)); delegateFunctions[1] = SquirrelCppHelper::DelegateFunction("sendSync", &HTTPRequest::SQUIRREL_METHOD_NAME(sendSync)); delegateFunctions[2] = SquirrelCppHelper::DelegateFunction("sendAsync", &HTTPRequest::SQUIRREL_METHOD_NAME(sendAsync)); delegateFunctions[3] = SquirrelCppHelper::DelegateFunction("setValidation", &HTTPRequest::SQUIRREL_METHOD_NAME(setValidation)); SquirrelCppHelper::registerDelegateInRegistry(vm, "HTTPRequest", delegateFunctions, 4); SquirrelCppHelper::assignDelegateFromRegistry(vm, "HTTPRequest"); } SQInteger result = request->constructRequest(vm, curlMulti, curlTemplate, verb, url, headers, body, bodySize); if(result < 0) { return result; } return 1; } // Squirrel Methods //----------------- /// Unimplemented - will ultimately cancel an ongoing async request. SQUIRREL_METHOD_IMPL(HTTPRequest, cancel) { return 0; } /// Will begin a sync request and return only once the request is complete. /// \returns the request result as a table of the format { "statusCode": nnn, "headers": {}, "body": "" }. /// \note If statusCode is < 100, it represents a Curl Error Code. SQUIRREL_METHOD_IMPL(HTTPRequest, sendSync) { if(request == nullptr || isMulti) { return sq_throwerror(vm, "HTTPRequest: Request has already been used"); } isMulti = false; CURLcode result = curl_easy_perform(request); return processResult(result); } /// Will begin an async request, triggering the provided doneCallback when complete. /// \param doneCallback a function that will be executed upon completion of the request, taking one parameter 'result' as a table of the format { "statusCode": nnn, "headers": {}, "body": "" }. /// \param streamCallback a function for streaming requests (OPTIONAL and UNUSED). /// \param timeout a timeout in seconds for streaming requests (OPTIONAL and UNUSED). /// \note If statusCode is < 100, it represents a Curl Error Code. SQUIRREL_METHOD_IMPL(HTTPRequest, sendAsync) { int types[] = {OT_CLOSURE, OT_CLOSURE, OT_INTEGER | OT_FLOAT}; if(SQ_FAILED(SquirrelCppHelper::checkParameterTypes(vm, 3, 1, types))) { return SQ_ERROR; } if(request == nullptr || isMulti) { return sq_throwerror(vm, "HTTPRequest: Request has already been used"); } // Fetch the method parameters sq_getstackobj(vm, 1, &self); sq_addref(vm, &self); sq_getstackobj(vm, 2, &doneCallback); sq_addref(vm, &doneCallback); sq_getstackobj(vm, 3, &streamCallback); sq_addref(vm, &streamCallback); //SQInteger streamTimeout; //sq_getinteger(vm, 4, &streamTimeout); isMulti = true; CURLMcode result = curl_multi_add_handle(curlMulti, request); return 0; } /// Unimplemented - will ultimately enforce HTTPS. SQUIRREL_METHOD_IMPL(HTTPRequest, setValidation) { return 0; } // Methods //-------- /// Processes the results of a completed Curl request, cleaning-up as required /// and constructing the result table to be placed onto the stack. /// \param result the Curl request result code (used for sync requests only). /// \returns places the result table onto the stack and returns '1' for sync requests or calls the doneCallback for async requests. SQInteger HTTPRequest::processResult(CURLcode result) { // Cleanup the CURL request (setting to nullptr to indicate complete) and free the read (sent) headers and data if(isMulti) { curl_multi_remove_handle(curlMulti, request); } curl_easy_cleanup(request); request = nullptr; curl_slist_free_all(readHeaders); if(readData != nullptr) { free(readData); } // Create a table to hold the results of the request sq_newtableex(vm, 3); // Store the response status code sq_pushstringex(vm, "statusCode", -1, SQTrue); if(result != CURLE_OK) { sq_pushinteger(vm, result); } else { sq_pushinteger(vm, responseStatusCode); } sq_newslot(vm, -3, false); // Store the write (received) data/body in the table and release the data structure sq_pushstringex(vm, "body", -1, SQTrue); sq_pushstring(vm, (const SQChar*)writeData, writeDataSize); sq_newslot(vm, -3, false); free(writeData); // Store the write (received) headers in the table and release the extra reference sq_pushstringex(vm, "headers", -1, SQTrue); sq_pushobject(vm, writeHeaders); sq_newslot(vm, -3, false); sq_release(vm, &writeHeaders); if(isMulti) { // Call the doneCallback sq_pushobject(vm, doneCallback); sq_pushroottable(vm); sq_push(vm, -3); if(SQ_FAILED(sq_call(vm,2,false,true))) { Log_Debug("Execution of doneCallback failed.\n"); return -1; } sq_pop(vm, 2); sq_release(vm, &self); if(doneCallback._type != OT_NULL) { sq_release(vm, &doneCallback); } if(streamCallback._type != OT_NULL) { sq_release(vm, &streamCallback); } sq_collectgarbage(vm); isMulti = false; } return 1; } /// Constructs the HTTPRequest. /// \param vm the VM in which to create internal objects and issue callbacks. /// \param curlMulti a reference to a CurlMulti handle to use in async requests. /// \param curlTemplate a template curl request, providing base/shared parameters for duplication. /// \param verb the verb of the HTTP request e.g. "GET". /// \param url the url of the HTTP request e.g. https://www.microsoft.com/ /// \param headers a Curl-compatible linked-list of HTTP headers to be sent as part of the request (must be created with curl_slist_append, HTTPRequest will take ownership and call curl_slist_free_all). /// \param body the data body to be sent as part of the request (must be created with sq_malloc, HTTPRequest will take ownership and call sq_free). /// \param bodySize the size of the data body. /// \returns SQ_OK on Success, otherwise SQ_ERROR. /// \throws Throws an error message in Squirrel upon error. SQInteger HTTPRequest::constructRequest(HSQUIRRELVM vm, CURLM *curlMulti, CURL *curlTemplate, const SQChar* verb, const SQChar* url, curl_slist *headers, SQChar *body, SQInteger bodySize) { // Create a new CURL request based on the provided template and its parameters request = curl_easy_init(); if(request == nullptr) { return sq_throwerror(vm, "Unable to create CURL handle"); } CURLcode result; result = curl_easy_setopt(request, CURLOPT_CUSTOMREQUEST, verb); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } result = curl_easy_setopt(request, CURLOPT_URL, url); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } result = curl_easy_setopt(request, CURLOPT_HTTPHEADER, headers); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } if(bodySize > 0) { result = curl_easy_setopt(request, CURLOPT_READDATA, (void*)this); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } result = curl_easy_setopt(request, CURLOPT_UPLOAD, 1L); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } result = curl_easy_setopt(request, CURLOPT_INFILESIZE, bodySize); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } } result = curl_easy_setopt(request, CURLOPT_WRITEDATA, (void*)this); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } result = curl_easy_setopt(request, CURLOPT_HEADERDATA, (void*)this); if(result != CURLcode::CURLE_OK) { curl_easy_cleanup(request); return sq_throwerror(vm, curl_easy_strerror(result)); } // Temporary options to deal with duphandle not working // Enable redirect following result = curl_easy_setopt(request, CURLOPT_FOLLOWLOCATION, 1L); // Restrict requests to HTTP(S) long allowedProtocols = CURLPROTO_HTTP | CURLPROTO_HTTPS; result = curl_easy_setopt(request, CURLOPT_PROTOCOLS, allowedProtocols); // Restrict redirects to HTTP(S) result = curl_easy_setopt(request, CURLOPT_REDIR_PROTOCOLS, allowedProtocols); // Load the HTTPS certificate(s) char *certificatePath = Storage_GetAbsolutePathInImagePackage("certs/CA.cer"); result = curl_easy_setopt(request, CURLOPT_CAINFO, certificatePath); // Turn of verbose messaging (for performance/space) result = curl_easy_setopt(request, CURLOPT_VERBOSE, 0L); //result = curl_easy_setopt(request, CURLOPT_DEBUGFUNCTION, HTTP::curlDebugCallback); // Use proxy //Networking_Curl_SetDefaultProxy(curlTemplate); // Attach function to handle downloaded data result = curl_easy_setopt(request, CURLOPT_WRITEFUNCTION, HTTP::curlWriteCallback); // Attach function to handle downloaded headers result = curl_easy_setopt(request, CURLOPT_HEADERFUNCTION, HTTP::curlWriteHeaderCallback); // Attach function to handle uploaded data result = curl_easy_setopt(request, CURLOPT_READFUNCTION, HTTP::curlReadCallback); // Attach a reference to this for use in Multi to determine the owning object of the curl easy request result = curl_easy_setopt(request, CURLOPT_PRIVATE, this); // Setup data storage isMulti = false; this->vm = vm; this->curlMulti = curlMulti; responseStatusCode = -1; readData = body; iReadData = body; readDataRemaining = bodySize; readHeaders = headers; writeData = (SQChar*)malloc(0); writeDataSize = 0; sq_newtable(vm); sq_resetobject(&writeHeaders); sq_getstackobj(vm, -1, &writeHeaders); sq_addref(vm, &writeHeaders); sq_poptop(vm); sq_resetobject(&doneCallback); sq_resetobject(&streamCallback); return SQ_OK; } /// Handles Curl read requests, providing the next piece of data to be sent as part of the request. /// \param buffer the read buffer provided by Curl for placing the request data into. /// \param maxTransferSize the maximum amount of space within buffer for this transfer. /// \returns the number of bytes placed/transfered into the buffer. size_t HTTPRequest::curlReadCallback(void *buffer, size_t maxTransferSize) { size_t transferSize = maxTransferSize < readDataRemaining ? maxTransferSize : readDataRemaining; memcpy(buffer, (void*)iReadData, transferSize); iReadData += transferSize; readDataRemaining -= transferSize; return transferSize; } /// Handles Curl write requests, expanding and placing request response data into a buffer. /// \param data the write buffer provided by Curl containing the data to be received. /// \param dataSize the amount of received data contained within the write buffer for receipt. /// \returns the number of bytes removed/received from the buffer. size_t HTTPRequest::curlWriteCallback(void *data, size_t dataSize) { writeData = (SQChar*)realloc((void*)writeData, writeDataSize + dataSize); memcpy((void*)(writeData + writeDataSize), data, dataSize); writeDataSize += dataSize; return dataSize; } /// Handles Curl write header request, receiving header data into a Squirrel table and extracting the response statusCode. /// \param buffer the write buffer provided by Curl containing the header data to be received. /// \param headerSize the amount of received header data contained within the write buffer for receipt. /// \returns the number of bytes removed/received from the buffer. /// \note We assume curl will only provide compliant output. size_t HTTPRequest::curlWriteHeaderCallback(void *buffer, size_t headerSize) { if(responseStatusCode == -1) { // Extract the status code from the header SQChar* space = (SQChar*)memchr(buffer, ' ', headerSize); *(space+4) = '\n'; responseStatusCode = atoi(space+1); } else { SQChar *colon = (SQChar*)memchr(buffer, ':', headerSize); if(colon != nullptr) { // Convert the buffer to lower-case for consistency for(size_t i = 0; i < headerSize; ++i) { ((SQChar*)buffer)[i] = tolower(((SQChar*)buffer)[i]); } // Place the header field into the headers table sq_pushobject(vm, writeHeaders); sq_pushstring(vm, (const SQChar*)buffer, colon-(SQChar*)buffer); // Strip leading whitespace from the header value for(++colon; colon < (SQChar*)buffer + headerSize; ++colon) { if(*colon != ' ') { break; } } // Place the header value into the headers table, removing the \r\n ending sq_pushstring(vm, (const SQChar*)colon, (((SQChar*)buffer)+headerSize)-(colon+2)); sq_newslot(vm,-3,false); sq_poptop(vm); } else if(headerSize == 2 && strncmp((const char*)buffer, "\r\n", 2) == 0 && responseStatusCode == 100) { responseStatusCode = -1; } } return headerSize; } // Destructor //----------- HTTPRequest::~HTTPRequest() { if(request) { if(isMulti) { curl_multi_remove_handle(curlMulti, request); if(doneCallback._type != OT_NULL) { sq_release(vm, &doneCallback); } if(streamCallback._type != OT_NULL) { sq_release(vm, &streamCallback); } isMulti = false; } curl_easy_cleanup(request); curl_slist_free_all(readHeaders); if(readData != nullptr) { free(readData); } free(writeData); sq_release(vm, &writeHeaders); } }