HTTPS_MutualAuth/Azsphere/main.c (317 lines of code) (raw):

/* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. */ // This sample C application for Azure Sphere periodically downloads and outputs the index web page // at example.com, by using cURL over a secure HTTPS connection. // It uses the cURL 'easy' API which is a synchronous (blocking) API. // // It uses the following Azure Sphere libraries: // - curl (URL transfer library) // - eventloop (system invokes handlers for timer events) // - log (displays messages in the Device Output window during debugging) // - networking (network ready) // - storage (device storage interaction) #include <errno.h> #include <getopt.h> #include <stdbool.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <curl/curl.h> // applibs_versions.h defines the API struct versions to use for applibs APIs. #include "applibs_versions.h" #include <applibs/log.h> #include <applibs/networking.h> #include <applibs/networking_curl.h> #include <applibs/storage.h> #include "eventloop_timer_utilities.h" /// <summary> /// Exit codes for this application. These are used for the /// application exit code. They must all be between zero and 255, /// where zero is reserved for successful termination. /// </summary> typedef enum { ExitCode_Success = 0, ExitCode_TermHandler_SigTerm = 1, ExitCode_TimerHandler_Consume = 2, ExitCode_Init_EventLoop = 3, ExitCode_Init_DownloadTimer = 4, ExitCode_Main_EventLoopFail = 5, ExitCode_IsNetworkingReady_Failed = 6, ExitCode_CurlSetDefaultProxy_Failed = 7 } ExitCode; /// <summary> /// Data pointer and size of a block of memory allocated on the heap. /// </summary> typedef struct { CURL *curlHandle; bool paused; char *data; size_t size; } MemoryBlock; static void TerminationHandler(int signalNumber); static size_t StoreDownloadedDataCallback(void *chunks, size_t chunkSize, size_t chunksCount, void *memoryBlock); static void LogCurlError(const char *message, int curlErrCode); static void PrintResponseAndResetData(MemoryBlock *block); static void PerformWebPageDownload(void); static void TimerEventHandler(EventLoopTimer *timer); static ExitCode InitHandlers(void); static void CloseHandlers(void); static bool IsNetworkReady(void); static int TransferInfoCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); static void ParseCommandLineArguments(int argc, char *argv[]); static EventLoop *eventLoop = NULL; static EventLoopTimer *downloadTimer = NULL; static volatile sig_atomic_t exitCode = ExitCode_Success; // The maximum number of characters which are printed from the HTTP response body. static const size_t MaxResponseCharsToPrint = 2048; // Flag to indicate if all content has been logged. static bool printedEntireResponse = false; // By default, do not bypass proxy. static bool bypassProxy = false; /// <summary> /// Signal handler for termination requests. This handler must be async-signal-safe. /// </summary> static void TerminationHandler(int signalNumber) { // Don't use Log_Debug here, as it is not guaranteed to be async-signal-safe. exitCode = ExitCode_TermHandler_SigTerm; } /// <summary> /// Callback for curl_easy_perform() that copies the downloaded chunks in a single memory /// block. If the size of the block exceeds MaxResponseCharsToPrint, then the download is /// paused. /// <param name="chunks">The pointer to the chunks array</param> /// <param name="chunkSize">The size of each chunk</param> /// <param name="chunksCount">The count of the chunks</param> /// <param name="memoryBlock">The pointer where all the downloaded chunks are aggregated</param> /// <returns> /// Number of additional bytes of memory successfully allocated, 0 if memory /// allocation fails or CURL_WRITEFUNC_PAUSE if size of block will exceed /// MaxResponseCharsToPrint. /// </returns> /// </summary> static size_t StoreDownloadedDataCallback(void *chunks, size_t chunkSize, size_t chunksCount, void *memoryBlock) { MemoryBlock *block = (MemoryBlock *)memoryBlock; size_t additionalDataSize = chunkSize * chunksCount; if ((block->size + additionalDataSize + 1) > MaxResponseCharsToPrint) { // Size of block will exceed MaxResponseCharsToPrint. Pause the download. block->paused = true; return CURL_WRITEFUNC_PAUSE; } char *tempDataPtr = realloc(block->data, block->size + additionalDataSize + 1); if (tempDataPtr == NULL) { Log_Debug("Out of memory, realloc returned NULL: errno=%d (%s)\n", errno, strerror(errno)); // Signal an error condition to the cURL library. This will cause the transfer to get // aborted. return 0; } // Memory successfully allocated. block->data = tempDataPtr; memcpy(block->data + block->size, chunks, additionalDataSize); block->size += additionalDataSize; block->data[block->size] = 0; // Ensure the block of memory is null terminated. return additionalDataSize; } /// <summary> /// Callback for CURLOPT_XFERINFOFUNCTION. This callback logs the content that is downloaded so /// far if the download has been paused. /// </summary> /// <param name="clientp">Pointer set with CURLOPT_XFERINFODATA</param> /// <param name="dltotal">Total number of bytes libcurl expects to download in this /// transfer.</param> /// <param name="dlnow">Number of bytes downloaded so far.</param> /// <param name="ultotal">Total number of bytes libcurl expects to upload in this transfer.</param> /// <param name="ulnow">Number of bytes uploaded so far.</param> /// <returns>0 to continue, or 1 to abort the transfer. </returns> static int TransferInfoCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { MemoryBlock *block = (MemoryBlock *)clientp; if (block == NULL) { Log_Debug("WARNING: Memory block received in TransferInfoCallback is NULL.\n"); return 0; } if (block->paused) { // Log the content downloaded so far. PrintResponseAndResetData(block); // Unpause the download. block->paused = false; CURLcode res = CURLE_OK; if ((res = curl_easy_pause(block->curlHandle, CURLPAUSE_CONT)) != CURLE_OK) { LogCurlError("curl_easy_pause CURLPAUSE_CONT", res); return 1; } } // If dltotal equals dlnow, it indicates that download has complete. Print the last bit of // response. // Note: dltotal will be 0 if the Content-Length entity-header is missing in the response // header. In that case, the last bit of response is logged after curl_easy_perform exits. if ((dltotal > 0) && (dltotal == dlnow) && (!printedEntireResponse)) { PrintResponseAndResetData(block); printedEntireResponse = true; } return 0; } /// <summary> /// Logs a cURL error. /// </summary> /// <param name="message">The message to print</param> /// <param name="curlErrCode">The cURL error code to describe</param> static void LogCurlError(const char *message, int curlErrCode) { Log_Debug(message); Log_Debug(" (curl err=%d, '%s')\n", curlErrCode, curl_easy_strerror(curlErrCode)); } /// <summary> /// Checks that the network is ready. /// </summary> static bool IsNetworkReady(void) { bool isNetworkReady = false; if (Networking_IsNetworkingReady(&isNetworkReady) == -1) { Log_Debug("ERROR: Networking_IsNetworkingReady: %d (%s)\n", errno, strerror(errno)); exitCode = ExitCode_IsNetworkingReady_Failed; return false; } if (!isNetworkReady) { Log_Debug("WARNING: Not doing download because the network is not ready.\n"); } return isNetworkReady; } /// <summary> /// Print the response contents and reset the data. /// </summary> /// <param name="block"> /// Pointer to the memory block that holds the data. /// </param> static void PrintResponseAndResetData(MemoryBlock *block) { if ((block == NULL) || (block->data == NULL)) { Log_Debug( "WARNING: Not printing and resetting data as either memory block or the data is " "null.\n"); return; } Log_Debug("%s", block->data); free(block->data); block->size = 0; block->data = NULL; } /// <summary> /// Download a web page over HTTPS protocol using cURL. /// </summary> static void PerformWebPageDownload(void) { CURL *curlHandle = NULL; CURLcode res = 0; MemoryBlock block = {.data = NULL, .size = 0, .paused = false}; char *certificatePath = NULL; char *clientCertPath = NULL; char *clientKeyPath = NULL; printedEntireResponse = false; if (IsNetworkReady() == false) { goto exitLabel; } Log_Debug("\n -===- START-OF-DOWNLOAD -===-\n"); // Init the cURL library. if ((res = curl_global_init(CURL_GLOBAL_ALL)) != CURLE_OK) { LogCurlError("curl_global_init", res); goto exitLabel; } if ((curlHandle = curl_easy_init()) == NULL) { Log_Debug("curl_easy_init() failed\n"); goto cleanupLabel; } // Set the cURL handle to allow access to the cURL handle in callbacks. block.curlHandle = curlHandle; // Specify URL to download. // Important: Any change in the domain name must be reflected in the AllowedConnections // capability in app_manifest.json. #warning Change the following line to specify the server address and delete this warning! if ((res = curl_easy_setopt(curlHandle, CURLOPT_URL, "https://CHANGE.THIS:5000/")) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_URL", res); goto cleanupLabel; } // Set output level to verbose. if ((res = curl_easy_setopt(curlHandle, CURLOPT_VERBOSE, 1L)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_VERBOSE", res); goto cleanupLabel; } // Get the full path to the certificate file used to authenticate the HTTPS server identity. certificatePath = Storage_GetAbsolutePathInImagePackage("certs/ca-bundle.pem"); if (certificatePath == NULL) { Log_Debug("The certificate path could not be resolved: errno=%d (%s)\n", errno, strerror(errno)); goto cleanupLabel; } // Set the path for the certificate file that cURL uses to validate the server certificate. if ((res = curl_easy_setopt(curlHandle, CURLOPT_CAINFO, certificatePath)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_CAINFO", res); goto cleanupLabel; } clientCertPath= Storage_GetAbsolutePathInImagePackage("certs/device-cert.pem"); if (clientCertPath == NULL) { Log_Debug("The client certificate path could not be resolved: errno=%d (%s)\n", errno, strerror(errno)); goto cleanupLabel; } if ((res = curl_easy_setopt(curlHandle, CURLOPT_SSLCERT, clientCertPath)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_SSLCERT", res); goto cleanupLabel; } clientKeyPath= Storage_GetAbsolutePathInImagePackage("certs/device-key.pem"); if (clientKeyPath == NULL) { Log_Debug("The client key path could not be resolved: errno=%d (%s)\n", errno, strerror(errno)); goto cleanupLabel; } if ((res = curl_easy_setopt(curlHandle, CURLOPT_SSLKEY, clientKeyPath)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_SSLKEY", res); goto cleanupLabel; } if ((res = curl_easy_setopt(curlHandle, CURLOPT_SSL_VERIFYPEER, 1L)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_SSL_VERIFYPEER", res); goto cleanupLabel; } if ((res = curl_easy_setopt(curlHandle, CURLOPT_SSL_VERIFYHOST, 1L)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_SSL_VERIFYHOST", res); goto cleanupLabel; } // Let cURL follow any HTTP 3xx redirects. // Important: Any redirection to different domain names requires that domain name to be added to // app_manifest.json. if ((res = curl_easy_setopt(curlHandle, CURLOPT_FOLLOWLOCATION, 1L)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_FOLLOWLOCATION", res); goto cleanupLabel; } // Set up callback for cURL to use when downloading data. if ((res = curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, StoreDownloadedDataCallback)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_WRITEFUNCTION", res); goto cleanupLabel; } // Set the custom parameter of the callback to the memory block. if ((res = curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, (void *)&block)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_WRITEDATA", res); goto cleanupLabel; } // Specify a user agent. if ((res = curl_easy_setopt(curlHandle, CURLOPT_USERAGENT, "libcurl-agent/1.0")) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_USERAGENT", res); goto cleanupLabel; } // Set up callback for cURL to report transfer information. if ((res = curl_easy_setopt(curlHandle, CURLOPT_XFERINFOFUNCTION, TransferInfoCallback)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_XFERINFOFUNCTION", res); goto cleanupLabel; } // Set the custom parameter of the callback to the memory block. It is not used by cURL but is // only passed along from the application to the callback. if ((res = curl_easy_setopt(curlHandle, CURLOPT_XFERINFODATA, (void *)&block)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_XFERINFODATA", res); goto cleanupLabel; } // Turn on the progress meter. This enables CURLOPT_XFERINFOFUNCTION callbacks. if ((res = curl_easy_setopt(curlHandle, CURLOPT_NOPROGRESS, 0L)) != CURLE_OK) { LogCurlError("curl_easy_setopt CURLOPT_NOPROGRESS", res); goto cleanupLabel; } // Configure the cURL handle to use the proxy. if (!bypassProxy) { if (Networking_Curl_SetDefaultProxy(curlHandle) != 0) { Log_Debug("Networking_Curl_SetDefaultProxy failed: errno=%d (%s)\n", errno, strerror(errno)); exitCode = ExitCode_CurlSetDefaultProxy_Failed; goto cleanupLabel; } } // When using libcurl, as with other networking applications, the Azure Sphere OS will // allocate socket buffers which are attributed to your application's RAM usage. You can tune // the size of these buffers to reduce the RAM footprint of your application as appropriate. // Refer to https://learn.microsoft.com/azure-sphere/app-development/ram-usage-best-practices // for further details. // Perform the download of the web page. if ((res = curl_easy_perform(curlHandle)) != CURLE_OK) { LogCurlError("curl_easy_perform", res); } else { if (!printedEntireResponse) { // Log the remaining downloaded content if it wasn't logged in TransferInfoCallback. PrintResponseAndResetData(&block); } } cleanupLabel: // Clean up allocated memory. free(block.data); free(certificatePath); // Clean up sample's cURL resources. curl_easy_cleanup(curlHandle); // Clean up cURL library's resources. curl_global_cleanup(); Log_Debug("\n -===- END-OF-DOWNLOAD -===-\n"); exitLabel: return; } /// <summary> /// The timer event handler. /// </summary> static void TimerEventHandler(EventLoopTimer *timer) { if (ConsumeEventLoopTimerEvent(timer) != 0) { exitCode = ExitCode_TimerHandler_Consume; return; } PerformWebPageDownload(); } /// <summary> /// Set up SIGTERM termination handler and event handlers. /// </summary> /// <returns> /// ExitCode_Success if all resources were allocated successfully; otherwise another /// ExitCode value which indicates the specific failure. /// </returns> static ExitCode InitHandlers(void) { struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler = TerminationHandler; sigaction(SIGTERM, &action, NULL); eventLoop = EventLoop_Create(); if (eventLoop == NULL) { Log_Debug("Could not create event loop.\n"); return ExitCode_Init_EventLoop; } // Issue an HTTPS request at the specified period. static const struct timespec tenSeconds = {.tv_sec = 10, .tv_nsec = 0}; downloadTimer = CreateEventLoopPeriodicTimer(eventLoop, &TimerEventHandler, &tenSeconds); if (downloadTimer == NULL) { return ExitCode_Init_DownloadTimer; } return ExitCode_Success; } /// <summary> /// Clean up the resources previously allocated. /// </summary> static void CloseHandlers(void) { DisposeEventLoopTimer(downloadTimer); EventLoop_Close(eventLoop); } /// <summary> /// Parse the command-line arguments given in the application manifest. /// </summary> static void ParseCommandLineArguments(int argc, char *argv[]) { int option = 0; static const struct option cmdLineOptions[] = { {.name = "BypassProxy", .has_arg = no_argument, .flag = NULL, .val = 'b'}, {.name = NULL, .has_arg = 0, .flag = NULL, .val = 0}}; // Loop over all of the options. while ((option = getopt_long(argc, argv, "b", cmdLineOptions, NULL)) != -1) { switch (option) { case 'b': Log_Debug("Bypass Proxy\n"); bypassProxy = true; break; default: // Unknown options are ignored. break; } } } /// <summary> /// Main entry point for this sample. /// </summary> int main(int argc, char *argv[]) { Log_Debug("cURL easy interface based application starting.\n"); Log_Debug("This sample periodically attempts to download a webpage, using curl's 'easy' API."); ParseCommandLineArguments(argc, argv); exitCode = InitHandlers(); if (exitCode == ExitCode_Success) { // Download the web page immediately. PerformWebPageDownload(); } // Use event loop to wait for events and trigger handlers, until an error or SIGTERM happens while (exitCode == ExitCode_Success) { EventLoop_Run_Result result = EventLoop_Run(eventLoop, -1, true); // Continue if interrupted by signal, e.g. due to breakpoint being set. if (result == EventLoop_Run_Failed && errno != EINTR) { exitCode = ExitCode_Main_EventLoopFail; } } CloseHandlers(); Log_Debug("Application exiting.\n"); return exitCode; }