From e19af866fa02d1651eb3debb7e7348fb95d0fde7 Mon Sep 17 00:00:00 2001 From: BolongZhang-AWS <88056513+BolongZhang-AWS@users.noreply.github.com> Date: Mon, 18 Jul 2022 10:39:21 -0700 Subject: [PATCH] HTTP proxy support (#290) * HTTP proxy support --- CMakeLists.txt | 2 +- README.md | 2 + docs/HTTP_PROXY.md | 77 ++++++ docs/PERMISSIONS.md | 1 + source/SharedCrtResourceManager.cpp | 36 +++ source/config/Config.cpp | 347 ++++++++++++++++++++++++++-- source/config/Config.h | 42 +++- source/main.cpp | 4 +- source/util/ProxyUtils.cpp | 51 ++++ source/util/ProxyUtils.h | 43 ++++ test/CMakeLists.txt | 2 +- test/config/TestConfig.cpp | 68 ++++++ test/util/TestProxyUtils.cpp | 88 +++++++ 13 files changed, 738 insertions(+), 25 deletions(-) create mode 100644 docs/HTTP_PROXY.md create mode 100644 source/util/ProxyUtils.cpp create mode 100644 source/util/ProxyUtils.h create mode 100644 test/util/TestProxyUtils.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 01f8ec5f..2e480061 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,7 +185,7 @@ endif () ############################################### ## Build the AWS IoT Device Client Executable # ############################################### -add_executable(${DC_PROJECT_NAME} ${DC_SRC}) +add_executable(${DC_PROJECT_NAME} ${DC_SRC} source/util/ProxyUtils.cpp source/util/ProxyUtils.h) set_target_properties(${DC_PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX) target_compile_definitions(${DC_PROJECT_NAME} PRIVATE "-DDEBUG_BUILD") ## We need to add the project binary directory to the list of include directories diff --git a/README.md b/README.md index 3895b861..3c11e82c 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ * [Sensor Publish Feature](source/sensor-publish/README.md) * [Provisioning with Secure Elements Feature](source/secure-element/README.md) * [Logging](source/logging/README.md) + * [HTTP Proxy](docs/HTTP_PROXY.md) * [Samples](source/samples/): - [MQTT Pub/Sub Sample Feature](source/samples/pubsub/README.md) * [Doxygen Documentation](docs/README.md) @@ -114,6 +115,7 @@ cmake --build . --target test-aws-iot-device-client * [File and Directory Permission Requirements](docs/PERMISSIONS.md) * [Environment Variables](docs/ENV.md) * [Version](docs/VERSION.md) +* [HTTP Proxy](docs/HTTP_PROXY.md) ## AWS IoT Features * [Jobs Feature Readme](source/jobs/README.md) diff --git a/docs/HTTP_PROXY.md b/docs/HTTP_PROXY.md new file mode 100644 index 00000000..7b3072e8 --- /dev/null +++ b/docs/HTTP_PROXY.md @@ -0,0 +1,77 @@ +# HTTP Proxy + +**Notice:** Running the AWS IoT Device Client will incur usage of AWS IoT services, and is likely to incur charges on your AWS account. Please refer the pricing pages for [AWS IoT Core](https://aws.amazon.com/iot-core/pricing/), [AWS IoT Device Management](https://aws.amazon.com/iot-device-management/pricing/), and [AWS IoT Device Defender](https://aws.amazon.com/iot-device-defender/pricing/) for more details. +* [HTTP Proxy](#http-proxy) + + [HTTP Proxy Support](#http-proxy-support) + - [How It Works](#how-it-works) + - [Configuring HTTP Proxy via the JSON configuration file](#configuring-http-proxy-via-the-json-configuration-file) + - [Sample HTTP proxy configuration](#sample-http-proxy-configuration) + - [Configuration options](#configuration-options) + - [Permissions](#permissions) + - [Overriding the default configuration file from CLI](#overriding-the-default-configuration-file-from-command-line-interface) + +[*Back To The Main Readme*](../../README.md) + +## HTTP Proxy Support +The AWS IoT Device Client is free, open-source, modular software written in C++ that customers compile and install on Embedded Linux based IoT devices to access AWS IoT Core, AWS IoT Device Management, and AWS IoT Device Defender features. A common setup in the IoT industry is to have devices in field behind an HTTP proxy where the devices could only connect to the internet via the proxy. The HTTP Proxy support feature allows customers to configure their HTTP proxy server with username and password as authentication method when executing device client sample features. + +### How It Works +The AWS IoT Device Client initiates the MQTT connection based on the implementation of aws-iot-device-sdk-cpp-v2. The HTTP proxy support for AWS IoT Device Client enables the tunneling proxy mode provided by the C++ SDK. Upon the start of the IoT Device Client, the `HttpProxyOptions` will be attached to the MQTT connection context together with the proxy IP address, port number, authentication information to connect to AWS IoT via an HTTP proxy. + +### Configuring HTTP Proxy via the JSON configuration file +An HTTP Proxy configuration file is a JSON document that uses parameters to describe the proxy options used to interact with AWS IoT. + +The default HTTP proxy config file should be placed on your device at `~/.aws-iot-device-client/http-proxy.conf`. AWS IoT Device Client would establish MQTT connect with HTTP proxy **only if the `http-proxy-enabled` flag in the configuration is set to `true`**. +#### Sample HTTP proxy configuration +``` +{ + "http-proxy-enabled": true, + "http-proxy-host": "10.0.0.140", + "http-proxy-port": "9999", + "http-proxy-auth-method": "UserNameAndPassword", + "http-proxy-username": "MyUserName", + "http-proxy-password": "password12345" +} +``` +#### Configuration options +**Required Parameters:** + +`http-proxy-enabled`: Whether or not the HTTP proxy is enabled (true/false). We read this value in as boolean so no double quotes required for the value field. + +`http-proxy-host`: The host IP address of the http proxy server. + +*Note:* It is required that you configure the proxy server's IP address within the reserved private IP address range. Valid range of the private IP address are including as follow: +``` +Class A: 10.0.0.0 to 10.255.255.255 +Class B: 172.16.0.0 to 172.31.255.255 +Class C: 192.168.0.0 to 192.168.255.255 +``` + +`http-proxy-port`: The port which the proxy server will listen on. Port number will be validated to make sure it falls into the valid range of 1 - 65535. Note that we read this field in string format so quotes are required. + +`http-proxy-auth-method`: Configure HTTP "Basic Authentication" username and password for accessing the proxy. Access is only granted for authenticated users. Field is case-sensitive. Valid values are `UserNameAndPassword` or `None`. + +**Optional Parameter:** + +`http-proxy-username`: Username for the HTTP proxy authentication. + +`http-proxy-password`: Password for the HTTP proxy authentication. + +####Permissions + +The HTTP proxy support for AWS IoT Device Client enforces chmod permission code of 600 on the HTTP proxy config files. It is also recommended to set chmod permission to 745 on the directory storing config files. + +For more information regarding permission recommandations, check out the [permission readme page]((docs/PERMISSIONS.md)) + +### Overriding the default configuration file from command line interface + +User can switch between different HTTP proxy config files without modifying the default config. To override the default HTTP proxy config under the file path `~/.aws-iot-device-client/http-proxy.conf`, start the AWS IoT Device Client with the `--http-proxy-config` parameter and the file path of the overriding HTTP proxy config file. + +``` +$ ./aws-iot-device-client --http-proxy-config ~/.aws-iot-device-client/MyProxyConfig1.conf +``` +*Note:* User still need to follow the example format of the HTTP proxy configuration file. HTTP proxy will be enabled only if the `http-proxy-enabled` is set to `true`. + +*Note:* File and parent folder permissions will also be enforced on the overriding configuration file. + +[*Back To The Top*](#http-proxy) diff --git a/docs/PERMISSIONS.md b/docs/PERMISSIONS.md index 85f5cbf8..e1a992cd 100644 --- a/docs/PERMISSIONS.md +++ b/docs/PERMISSIONS.md @@ -25,6 +25,7 @@ Config File | 640 | **Yes** Pub/Sub Files | 600 | **Yes** Sensor Pubilsh Pathname Socket | 660 | **Yes** PKCS11 Library File | 640 | **Yes** +HTTP Proxy Config File | 600 | **Yes** #### Recommended and Required permissions on directories storing respective files Directory | Chmod Permissions | Required | diff --git a/source/SharedCrtResourceManager.cpp b/source/SharedCrtResourceManager.cpp index 8b537678..fa2fed38 100644 --- a/source/SharedCrtResourceManager.cpp +++ b/source/SharedCrtResourceManager.cpp @@ -3,8 +3,10 @@ #include "SharedCrtResourceManager.h" #include "Version.h" +#include "aws/crt/http/HttpProxyStrategy.h" #include "logging/LoggerFactory.h" #include "util/FileUtils.h" +#include "util/ProxyUtils.h" #include "util/Retry.h" #include "util/StringUtils.h" @@ -307,6 +309,40 @@ int SharedCrtResourceManager::establishConnection(const PlainConfig &config) clientConfigBuilder.WithSdkName(SharedCrtResourceManager::BINARY_NAME); clientConfigBuilder.WithSdkVersion(DEVICE_CLIENT_VERSION); + PlainConfig::HttpProxyConfig proxyConfig = config.httpProxyConfig; + Aws::Crt::Http::HttpClientConnectionProxyOptions proxyOptions; + + if (proxyConfig.httpProxyEnabled) + { + proxyOptions.HostName = proxyConfig.proxyHost->c_str(); + proxyOptions.Port = proxyConfig.proxyPort.value(); + + LOGM_INFO( + TAG, + "Attempting to establish MQTT connection with proxy: %s:%u", + proxyConfig.proxyHost->c_str(), + proxyConfig.proxyPort.value()); + + if (proxyConfig.httpProxyAuthEnabled) + { + LOG_INFO(TAG, "Proxy Authentication is enabled"); + Aws::Crt::Http::HttpProxyStrategyBasicAuthConfig basicAuthConfig; + basicAuthConfig.ConnectionType = Aws::Crt::Http::AwsHttpProxyConnectionType::Tunneling; + proxyOptions.AuthType = Aws::Crt::Http::AwsHttpProxyAuthenticationType::Basic; + basicAuthConfig.Username = proxyConfig.proxyUsername->c_str(); + basicAuthConfig.Password = proxyConfig.proxyPassword->c_str(); + proxyOptions.ProxyStrategy = + Aws::Crt::Http::HttpProxyStrategy::CreateBasicHttpProxyStrategy(basicAuthConfig, Aws::Crt::g_allocator); + } + else + { + LOG_INFO(TAG, "Proxy Authentication is disabled"); + proxyOptions.AuthType = Aws::Crt::Http::AwsHttpProxyAuthenticationType::None; + } + + clientConfigBuilder.WithHttpProxyOptions(proxyOptions); + } + auto clientConfig = clientConfigBuilder.Build(); if (!clientConfig) diff --git a/source/config/Config.cpp b/source/config/Config.cpp index 862b9e32..fe31656b 100644 --- a/source/config/Config.cpp +++ b/source/config/Config.cpp @@ -19,6 +19,7 @@ #include "../util/FileUtils.h" #include "../util/MqttUtils.h" +#include "../util/ProxyUtils.h" #include "../util/StringUtils.h" #include "Version.h" @@ -278,7 +279,8 @@ bool PlainConfig::LoadFromCliArgs(const CliArgs &cliArgs) return logConfig.LoadFromCliArgs(cliArgs) && jobs.LoadFromCliArgs(cliArgs) && tunneling.LoadFromCliArgs(cliArgs) && deviceDefender.LoadFromCliArgs(cliArgs) && fleetProvisioning.LoadFromCliArgs(cliArgs) && pubSub.LoadFromCliArgs(cliArgs) && sampleShadow.LoadFromCliArgs(cliArgs) && - configShadow.LoadFromCliArgs(cliArgs) && secureElement.LoadFromCliArgs(cliArgs); + configShadow.LoadFromCliArgs(cliArgs) && secureElement.LoadFromCliArgs(cliArgs) && + httpProxyConfig.LoadFromCliArgs(cliArgs); } bool PlainConfig::LoadFromEnvironment() @@ -1133,6 +1135,207 @@ bool PlainConfig::FleetProvisioningRuntimeConfig::Validate() const !thingName->empty(); } +constexpr char PlainConfig::HttpProxyConfig::CLI_HTTP_PROXY_CONFIG_PATH[]; +constexpr char PlainConfig::HttpProxyConfig::JSON_KEY_HTTP_PROXY_ENABLED[]; +constexpr char PlainConfig::HttpProxyConfig::JSON_KEY_HTTP_PROXY_HOST[]; +constexpr char PlainConfig::HttpProxyConfig::JSON_KEY_HTTP_PROXY_PORT[]; +constexpr char PlainConfig::HttpProxyConfig::JSON_KEY_HTTP_PROXY_AUTH_METHOD[]; +constexpr char PlainConfig::HttpProxyConfig::JSON_KEY_HTTP_PROXY_USERNAME[]; +constexpr char PlainConfig::HttpProxyConfig::JSON_KEY_HTTP_PROXY_PASSWORD[]; + +bool PlainConfig::HttpProxyConfig::LoadFromJson(const Crt::JsonView &json) +{ + const char *jsonKey = JSON_KEY_HTTP_PROXY_ENABLED; + if (json.ValueExists(jsonKey)) + { + httpProxyEnabled = json.GetBool(jsonKey); + } + + if (httpProxyEnabled) + { + jsonKey = JSON_KEY_HTTP_PROXY_HOST; + if (json.ValueExists(jsonKey)) + { + if (!json.GetString(jsonKey).empty()) + { + proxyHost = json.GetString(jsonKey).c_str(); + } + else + { + LOGM_WARN( + Config::TAG, "Key {%s} was provided in the JSON configuration file with an empty value", jsonKey); + } + } + + jsonKey = JSON_KEY_HTTP_PROXY_PORT; + if (json.ValueExists(jsonKey)) + { + if (!json.GetString(jsonKey).empty()) + { + try + { + proxyPort = stoi(json.GetString(jsonKey).c_str()); + } + catch (...) + { + LOGM_ERROR( + Config::TAG, + "*** %s: Failed to convert JSON key {%s} to integer, please use a " + "valid value for port number", + DeviceClient::DC_FATAL_ERROR, + jsonKey); + return false; + } + } + else + { + LOGM_WARN( + Config::TAG, "Key {%s} was provided in the JSON configuration file with an empty value", jsonKey); + } + } + + jsonKey = JSON_KEY_HTTP_PROXY_AUTH_METHOD; + if (json.ValueExists(jsonKey)) + { + if (!json.GetString(jsonKey).empty()) + { + proxyAuthMethod = json.GetString(jsonKey).c_str(); + if (strcmp(proxyAuthMethod->c_str(), "UserNameAndPassword") == 0) + { + httpProxyAuthEnabled = true; + } + else if (strcmp(proxyAuthMethod->c_str(), "None") != 0) + { + LOGM_WARN( + Config::TAG, + "Unrecognized HTTP Proxy Authentication Method value: {%s}. Supported values are " + "UserNameAndPassword or None", + proxyAuthMethod->c_str()); + } + } + else + { + LOGM_WARN( + Config::TAG, "Key {%s} was provided in the JSON configuration file with an empty value", jsonKey); + } + } + + jsonKey = JSON_KEY_HTTP_PROXY_USERNAME; + if (json.ValueExists(jsonKey)) + { + if (!json.GetString(jsonKey).empty()) + { + proxyUsername = json.GetString(jsonKey).c_str(); + } + else + { + LOGM_WARN( + Config::TAG, "Key {%s} was provided in the JSON configuration file with an empty value", jsonKey); + } + } + + jsonKey = JSON_KEY_HTTP_PROXY_PASSWORD; + if (json.ValueExists(jsonKey)) + { + if (!json.GetString(jsonKey).empty()) + { + proxyPassword = json.GetString(jsonKey).c_str(); + } + else + { + LOGM_WARN( + Config::TAG, "Key {%s} was provided in the JSON configuration file with an empty value", jsonKey); + } + } + } + else + { + LOG_INFO(Config::TAG, "HTTP Proxy is disabled as configured."); + } + + return true; +} + +bool PlainConfig::HttpProxyConfig::LoadFromCliArgs(const CliArgs &cliArgs) +{ + if (cliArgs.count(PlainConfig::HttpProxyConfig::CLI_HTTP_PROXY_CONFIG_PATH)) + { + proxyConfigPath = + FileUtils::ExtractExpandedPath(cliArgs.at(PlainConfig::HttpProxyConfig::CLI_HTTP_PROXY_CONFIG_PATH)) + .c_str(); + } + else + { + // If http proxy config file path is not provided, + proxyConfigPath = Config::DEFAULT_HTTP_PROXY_CONFIG_FILE; + } + return true; +} + +bool PlainConfig::HttpProxyConfig::Validate() const +{ + if (!httpProxyEnabled) + { + return true; + } + + if (!proxyHost.has_value() || proxyHost->empty()) + { + LOGM_ERROR( + Config::TAG, + "*** %s: Proxy host name field must be specified if HTTP proxy is enabled ***", + DeviceClient::DC_FATAL_ERROR); + return false; + } + + if (!ProxyUtils::ValidateHostIpAddress(proxyHost->c_str())) + { + LOGM_ERROR( + Config::TAG, + "*** %s: Proxy host IP address must be a private IP address ***", + DeviceClient::DC_FATAL_ERROR); + return false; + } + + if (!proxyPort.has_value() || !ProxyUtils::ValidatePortNumber(proxyPort.value())) + { + LOGM_ERROR( + Config::TAG, + "*** %s: Valid value of proxy port field must be specified if HTTP proxy is enabled ***", + DeviceClient::DC_FATAL_ERROR); + return false; + } + + if (!proxyAuthMethod.has_value() || proxyAuthMethod->empty()) + { + LOGM_ERROR( + Config::TAG, + "*** %s: Proxy auth method field must be specified if HTTP proxy is enabled ***", + DeviceClient::DC_FATAL_ERROR); + return true; + } + + if (httpProxyAuthEnabled && (!proxyUsername.has_value() || proxyUsername->empty())) + { + LOGM_ERROR( + Config::TAG, + "*** %s: Proxy username field must be specified if HTTP proxy authentication is enabled ***", + DeviceClient::DC_FATAL_ERROR); + return false; + } + + if (httpProxyAuthEnabled && (!proxyPassword.has_value() || proxyPassword->empty())) + { + LOGM_ERROR( + Config::TAG, + "*** %s: Proxy password field must be specified if HTTP proxy authentication is enabled ***", + DeviceClient::DC_FATAL_ERROR); + return false; + } + + return true; +} + constexpr char PlainConfig::PubSub::CLI_ENABLE_PUB_SUB[]; constexpr char PlainConfig::PubSub::CLI_PUB_SUB_PUBLISH_TOPIC[]; constexpr char PlainConfig::PubSub::CLI_PUB_SUB_PUBLISH_FILE[]; @@ -2049,6 +2252,7 @@ constexpr char Config::CLI_CONFIG_FILE[]; constexpr char Config::DEFAULT_FLEET_PROVISIONING_RUNTIME_CONFIG_FILE[]; constexpr char Config::DEFAULT_SAMPLE_SHADOW_OUTPUT_DIR[]; constexpr char Config::DEFAULT_SAMPLE_SHADOW_DOCUMENT_FILE[]; +constexpr char Config::DEFAULT_HTTP_PROXY_CONFIG_FILE[]; bool Config::CheckTerminalArgs(int argc, char **argv) { @@ -2130,7 +2334,8 @@ bool Config::ParseCliArgs(int argc, char **argv, CliArgs &cliArgs) {PlainConfig::SecureElement::CLI_SECURE_ELEMENT_PIN, true, nullptr}, {PlainConfig::SecureElement::CLI_SECURE_ELEMENT_KEY_LABEL, true, nullptr}, {PlainConfig::SecureElement::CLI_SECURE_ELEMENT_SLOT_ID, true, nullptr}, - {PlainConfig::SecureElement::CLI_SECURE_ELEMENT_TOKEN_LABEL, true, nullptr}}; + {PlainConfig::SecureElement::CLI_SECURE_ELEMENT_TOKEN_LABEL, true, nullptr}, + {PlainConfig::HttpProxyConfig::CLI_HTTP_PROXY_CONFIG_PATH, true, nullptr}}; map argumentDefinitionMap; for (auto &i : argumentDefinitions) @@ -2210,7 +2415,7 @@ bool Config::init(const CliArgs &cliArgs) bReadConfigFile = true; } - if (bReadConfigFile && !ParseConfigFile(filename, false)) + if (bReadConfigFile && !ParseConfigFile(filename, DEVICE_CLIENT_ESSENTIAL_CONFIG)) { LOGM_ERROR( TAG, @@ -2230,7 +2435,8 @@ bool Config::init(const CliArgs &cliArgs) return false; } - if (ParseConfigFile(Config::DEFAULT_FLEET_PROVISIONING_RUNTIME_CONFIG_FILE, true) && + if (ParseConfigFile( + Config::DEFAULT_FLEET_PROVISIONING_RUNTIME_CONFIG_FILE, FLEET_PROVISIONING_RUNTIME_CONFIG) && ValidateAndStoreRuntimeConfig()) { LOGM_INFO( @@ -2239,12 +2445,30 @@ bool Config::init(const CliArgs &cliArgs) Config::DEFAULT_FLEET_PROVISIONING_RUNTIME_CONFIG_FILE); } + if (ParseConfigFile(config.httpProxyConfig.proxyConfigPath->c_str(), HTTP_PROXY_CONFIG) && + config.httpProxyConfig.httpProxyEnabled) + { + if (!ValidateAndStoreHttpProxyConfig()) + { + LOGM_ERROR( + TAG, + "*** %s: Unable to Parse HTTP proxy Config file: '%s' ***", + DeviceClient::DC_FATAL_ERROR, + Sanitize(config.httpProxyConfig.proxyConfigPath->c_str()).c_str()); + return false; + } + LOGM_INFO( + TAG, + "Successfully fetched http proxy config file '%s' and validated its content.", + config.httpProxyConfig.proxyConfigPath->c_str()); + return true; + } + return config.Validate(); } catch (const std::exception &e) { LOGM_ERROR(TAG, "Error while initializing configuration: %s", e.what()); - return false; } } @@ -2267,21 +2491,54 @@ bool Config::ValidateAndStoreRuntimeConfig() return true; } -bool Config::ParseConfigFile(const string &file, bool isRuntimeConfig) +bool Config::ValidateAndStoreHttpProxyConfig() +{ + // check if all values are present and also check if the files are present then only overwrite values + if (!config.httpProxyConfig.Validate()) + { + LOGM_ERROR( + TAG, + "Failed to Validate http proxy configurations. Please check '%s' file", + Config::DEFAULT_HTTP_PROXY_CONFIG_FILE); + return false; + } + + return true; +} + +bool Config::ParseConfigFile(const string &file, ConfigFileType configFileType) { string expandedPath = FileUtils::ExtractExpandedPath(file.c_str()); if (!FileUtils::FileExists(expandedPath)) { - if (!isRuntimeConfig) - { - LOGM_DEBUG(TAG, "Unable to open config file %s, file does not exist", Sanitize(expandedPath).c_str()); - } - else + switch (configFileType) { - LOG_DEBUG( - TAG, - "Did not find a runtime configuration file, assuming Fleet Provisioning has not run for this device"); - } + case DEVICE_CLIENT_ESSENTIAL_CONFIG: + { + LOGM_DEBUG(TAG, "Unable to open config file %s, file does not exist", Sanitize(expandedPath).c_str()); + break; + } + case FLEET_PROVISIONING_RUNTIME_CONFIG: + { + LOG_DEBUG( + TAG, + "Did not find a runtime configuration file, assuming Fleet Provisioning has not run for this " + "device"); + break; + } + case HTTP_PROXY_CONFIG: + { + LOGM_DEBUG( + TAG, + "Did not find a http proxy config file %s, assuming HTTP proxy is disabled on this device", + Sanitize(expandedPath).c_str()); + break; + } + default: + { + LOG_ERROR(TAG, "Unhandled config file type when trying to load from disk: file does not exist"); + } + }; return false; } @@ -2299,8 +2556,32 @@ bool Config::ParseConfigFile(const string &file, bool isRuntimeConfig) } string configFileParentDir = FileUtils::ExtractParentDirectory(expandedPath.c_str()); - FileUtils::ValidateFilePermissions(configFileParentDir, Permissions::CONFIG_DIR, false); - FileUtils::ValidateFilePermissions(expandedPath.c_str(), Permissions::CONFIG_FILE, false); + FileUtils::ValidateFilePermissions(configFileParentDir, Permissions::CONFIG_DIR, true); + switch (configFileType) + { + case DEVICE_CLIENT_ESSENTIAL_CONFIG: + { + FileUtils::ValidateFilePermissions(expandedPath.c_str(), Permissions::CONFIG_FILE, true); + break; + } + case FLEET_PROVISIONING_RUNTIME_CONFIG: + { + FileUtils::ValidateFilePermissions(expandedPath.c_str(), Permissions::RUNTIME_CONFIG_FILE, true); + break; + } + case HTTP_PROXY_CONFIG: + { + FileUtils::ValidateFilePermissions(expandedPath.c_str(), Permissions::HTTP_PROXY_CONFIG_FILE, true); + break; + } + default: + { + LOGM_ERROR( + TAG, + "Undefined config type of file %s, file permission was not able to be verified", + expandedPath.c_str()); + } + } ifstream setting(expandedPath.c_str()); if (!setting.is_open()) @@ -2318,8 +2599,30 @@ bool Config::ParseConfigFile(const string &file, bool isRuntimeConfig) return false; } Aws::Crt::JsonView jsonView = Aws::Crt::JsonView(jsonObj); - // Parse, validate and store config file content - config.LoadFromJson(jsonView); + switch (configFileType) + { + case DEVICE_CLIENT_ESSENTIAL_CONFIG: + { + config.LoadFromJson(jsonView); + break; + } + case FLEET_PROVISIONING_RUNTIME_CONFIG: + { + break; + } + case HTTP_PROXY_CONFIG: + { + config.httpProxyConfig.LoadFromJson(jsonView); + break; + } + default: + { + LOGM_ERROR( + TAG, + "Undefined config type of file %s, was not able to parse config into json object", + expandedPath.c_str()); + } + } LOGM_INFO(TAG, "Successfully fetched JSON config file: %s", Sanitize(contents).c_str()); setting.close(); @@ -2389,7 +2692,8 @@ void Config::PrintHelpMessage() "%s :\t\t\t\t\tThe user PIN for logging into PKCS#11 token.\n" "%s :\t\t\t\t\tThe Label of private key on the PKCS#11 token (optional). \n" "%s :\t\t\t\t\tThe Slot ID containing PKCS#11 token to use (optional).\n" - "%s :\t\t\t\t\tThe Label of the PKCS#11 token to use (optional).\n"; + "%s :\t\t\t\t\tThe Label of the PKCS#11 token to use (optional).\n" + "%s :\t\t\t\tUse specified file path to load HTTP proxy configs\n"; cout << FormatMessage( helpMessageTemplate, @@ -2436,7 +2740,8 @@ void Config::PrintHelpMessage() PlainConfig::SecureElement::CLI_SECURE_ELEMENT_PIN, PlainConfig::SecureElement::CLI_SECURE_ELEMENT_KEY_LABEL, PlainConfig::SecureElement::CLI_SECURE_ELEMENT_SLOT_ID, - PlainConfig::SecureElement::CLI_SECURE_ELEMENT_TOKEN_LABEL); + PlainConfig::SecureElement::CLI_SECURE_ELEMENT_TOKEN_LABEL, + PlainConfig::HttpProxyConfig::CLI_HTTP_PROXY_CONFIG_PATH); } void Config::PrintVersion() diff --git a/source/config/Config.h b/source/config/Config.h index aca9c653..e32d7c12 100644 --- a/source/config/Config.h +++ b/source/config/Config.h @@ -60,6 +60,7 @@ namespace Aws static constexpr int SENSOR_PUBLISH_ADDR_FILE = 660; static constexpr int SENSOR_PUBLISH_ADDR_DIR = 700; static constexpr int PKCS11_LIB_FILE = 640; + static constexpr int HTTP_PROXY_CONFIG_FILE = 600; }; struct PlainConfig : public LoadableFromJsonAndCliAndEnvironment @@ -261,6 +262,33 @@ namespace Aws }; FleetProvisioningRuntimeConfig fleetProvisioningRuntimeConfig; + struct HttpProxyConfig : public LoadableFromJsonAndCliAndEnvironment + { + bool LoadFromJson(const Crt::JsonView &json) override; + bool LoadFromCliArgs(const CliArgs &cliArgs) override; + bool LoadFromEnvironment() override { return true; } + bool Validate() const override; + + static constexpr char CLI_HTTP_PROXY_CONFIG_PATH[] = "--http-proxy-config"; + + static constexpr char JSON_KEY_HTTP_PROXY_ENABLED[] = "http-proxy-enabled"; + static constexpr char JSON_KEY_HTTP_PROXY_HOST[] = "http-proxy-host"; + static constexpr char JSON_KEY_HTTP_PROXY_PORT[] = "http-proxy-port"; + static constexpr char JSON_KEY_HTTP_PROXY_AUTH_METHOD[] = "http-proxy-auth-method"; + static constexpr char JSON_KEY_HTTP_PROXY_USERNAME[] = "http-proxy-username"; + static constexpr char JSON_KEY_HTTP_PROXY_PASSWORD[] = "http-proxy-password"; + + bool httpProxyEnabled{false}; + bool httpProxyAuthEnabled{false}; + Aws::Crt::Optional proxyConfigPath; + Aws::Crt::Optional proxyHost; + Aws::Crt::Optional proxyPort; + Aws::Crt::Optional proxyAuthMethod; + Aws::Crt::Optional proxyUsername; + Aws::Crt::Optional proxyPassword; + }; + HttpProxyConfig httpProxyConfig; + struct PubSub : public LoadableFromJsonAndCliAndEnvironment { bool LoadFromJson(const Crt::JsonView &json) override; @@ -442,6 +470,7 @@ namespace Aws static constexpr char DEFAULT_CONFIG_FILE[] = "~/.aws-iot-device-client/aws-iot-device-client.conf"; static constexpr char DEFAULT_FLEET_PROVISIONING_RUNTIME_CONFIG_FILE[] = "~/.aws-iot-device-client/aws-iot-device-client-runtime.conf"; + static constexpr char DEFAULT_HTTP_PROXY_CONFIG_FILE[] = "~/.aws-iot-device-client/http-proxy.conf"; static constexpr char DEFAULT_SAMPLE_SHADOW_OUTPUT_DIR[] = "~/.aws-iot-device-client/sample-shadow/"; static constexpr char DEFAULT_SAMPLE_SHADOW_DOCUMENT_FILE[] = "default-sample-shadow-document"; @@ -450,10 +479,21 @@ namespace Aws static constexpr char CLI_EXPORT_DEFAULT_SETTINGS[] = "--export-default-settings"; static constexpr char CLI_CONFIG_FILE[] = "--config-file"; + /** + * \brief Enum defining several config file types + */ + enum ConfigFileType + { + DEVICE_CLIENT_ESSENTIAL_CONFIG, + FLEET_PROVISIONING_RUNTIME_CONFIG, + HTTP_PROXY_CONFIG + }; + static bool CheckTerminalArgs(int argc, char *argv[]); static bool ParseCliArgs(int argc, char *argv[], CliArgs &cliArgs); bool ValidateAndStoreRuntimeConfig(); - bool ParseConfigFile(const std::string &file, bool isRuntimeConfig); + bool ValidateAndStoreHttpProxyConfig(); + bool ParseConfigFile(const std::string &file, ConfigFileType configFileType); bool init(const CliArgs &cliArgs); /** diff --git a/source/main.cpp b/source/main.cpp index 478a65f3..853094ce 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -389,7 +389,9 @@ int main(int argc, char *argv[]) */ FleetProvisioning fleetProvisioning; if (!fleetProvisioning.ProvisionDevice(resourceManager, config.config) || - !config.ParseConfigFile(Config::DEFAULT_FLEET_PROVISIONING_RUNTIME_CONFIG_FILE, true) || + !config.ParseConfigFile( + Config::DEFAULT_FLEET_PROVISIONING_RUNTIME_CONFIG_FILE, + Aws::Iot::DeviceClient::Config::FLEET_PROVISIONING_RUNTIME_CONFIG) || !config.ValidateAndStoreRuntimeConfig()) { LOGM_ERROR( diff --git a/source/util/ProxyUtils.cpp b/source/util/ProxyUtils.cpp new file mode 100644 index 00000000..05b880a3 --- /dev/null +++ b/source/util/ProxyUtils.cpp @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0// Created by Zhang, Bolong on 6/6/22. + +#include "ProxyUtils.h" +#include "../logging/LoggerFactory.h" +#include + +using namespace Aws::Iot::DeviceClient::Util; +using namespace Aws::Iot::DeviceClient::Logging; + +static constexpr char TAG[] = "ProxyUtils.cpp"; + +bool ProxyUtils::ValidatePortNumber(const int portNumber) +{ + if (portNumber < 1 || portNumber > UINT16_MAX) + { + LOGM_ERROR(TAG, "Port number %u exceeded valid range", portNumber); + return false; + } + return true; +} + +bool ProxyUtils::ValidateHostIpAddress(const std::string &ipAddress) +{ + + /** + * inet_pton() was used here to validate the IP address format, it also convert the IP address to a numeric format. + * AF_INET is the address family that supports IPv4, + * the in_addr structure will be populated with the address information. + * + * It returns 1 on success, 0 or -1 for varies of reasons of invalid conversion. + */ + struct in_addr addr; + // cppcheck-suppress variableScope + uint32_t IPv4Identifier; + int result = inet_pton(AF_INET, ipAddress.c_str(), &(addr)); + + if (result > 0) + { + IPv4Identifier = ntohl(*((uint32_t *)&(addr))); + return IsIpAddressPrivate(IPv4Identifier); + } + return false; +} + +bool ProxyUtils::IsIpAddressPrivate(std::uint32_t ipAddress) +{ + return (ipAddress >= DECIMAL_REP_IP_10_0_0_0 && ipAddress <= DECIMAL_REP_IP_10_255_255_255) || + (ipAddress >= DECIMAL_REP_IP_172_16_0_0 && ipAddress <= DECIMAL_REP_IP_172_31_255_255) || + (ipAddress >= DECIMAL_REP_IP_192_168_0_0 && ipAddress <= DECIMAL_REP_IP_192_168_255_255); +} diff --git a/source/util/ProxyUtils.h b/source/util/ProxyUtils.h new file mode 100644 index 00000000..6eba5dd5 --- /dev/null +++ b/source/util/ProxyUtils.h @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +namespace Aws +{ + namespace Iot + { + namespace DeviceClient + { + namespace Util + { + namespace ProxyUtils + { + static constexpr uint32_t DECIMAL_REP_IP_10_0_0_0 = 167772160; + static constexpr uint32_t DECIMAL_REP_IP_10_255_255_255 = 184549375; + static constexpr uint32_t DECIMAL_REP_IP_172_16_0_0 = 2886729728; + static constexpr uint32_t DECIMAL_REP_IP_172_31_255_255 = 2887778303; + static constexpr uint32_t DECIMAL_REP_IP_192_168_0_0 = 3232235520; + static constexpr uint32_t DECIMAL_REP_IP_192_168_255_255 = 3232301055; + /** + * \brief Validates port number is in the valid range + * + * @param port port number + * @return returns true if port number is in the valid range + */ + bool ValidatePortNumber(int portNumber); + + /** + * \brief Validates IP address is in the reserved private block range + * + * @param ipAddress IP address + * @return returns true if IP is in the valid range + */ + bool ValidateHostIpAddress(const std::string &ipAddress); + + bool IsIpAddressPrivate(std::uint32_t ipAddress); + } // namespace ProxyUtils + } // namespace Util + } // namespace DeviceClient + } // namespace Iot +} // namespace Aws diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e7f84d47..c6ed181a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -103,7 +103,7 @@ list(APPEND DC_SRC ${DC_TST}) list(FILTER DC_SRC EXCLUDE REGEX ".*main.cpp$") set(HEADERS) -add_executable(${GTEST_PROJECT} ${DC_SRC}) +add_executable(${GTEST_PROJECT} ${DC_SRC} util/TestProxyUtils.cpp) if (NOT EXCLUDE_JOBS) target_link_libraries(${GTEST_PROJECT} IotJobs-cpp) diff --git a/test/config/TestConfig.cpp b/test/config/TestConfig.cpp index 0e1278b1..bcebb75d 100644 --- a/test/config/TestConfig.cpp +++ b/test/config/TestConfig.cpp @@ -1618,6 +1618,74 @@ TEST_F(ConfigTestFixture, SecureElementCli) } #endif +TEST_F(ConfigTestFixture, HTTPProxyConfigHappy) +{ + constexpr char jsonString[] = R"( +{ + "http-proxy-enabled": true, + "http-proxy-host": "10.0.0.1", + "http-proxy-port": "8888", + "http-proxy-auth-method": "UserNameAndPassword", + "http-proxy-username": "testUserName", + "http-proxy-password": "12345" +})"; + JsonObject jsonObject(jsonString); + JsonView jsonView = jsonObject.View(); + + PlainConfig::HttpProxyConfig httpProxyConfig; + httpProxyConfig.LoadFromJson(jsonView); + + ASSERT_TRUE(httpProxyConfig.httpProxyEnabled); + ASSERT_STREQ("10.0.0.1", httpProxyConfig.proxyHost->c_str()); + ASSERT_EQ(8888, httpProxyConfig.proxyPort.value()); + ASSERT_TRUE(httpProxyConfig.httpProxyAuthEnabled); + ASSERT_STREQ("UserNameAndPassword", httpProxyConfig.proxyAuthMethod->c_str()); + ASSERT_STREQ("testUserName", httpProxyConfig.proxyUsername->c_str()); + ASSERT_STREQ("12345", httpProxyConfig.proxyPassword->c_str()); +} + +TEST_F(ConfigTestFixture, HTTPProxyConfigDisabled) +{ + constexpr char jsonString[] = R"( +{ + "http-proxy-enabled": false, + "http-proxy-host": "10.0.0.1", + "http-proxy-port": "8888", + "http-proxy-auth-method": "UserNameAndPassword", + "http-proxy-username": "testUserName", + "http-proxy-password": "12345" +})"; + JsonObject jsonObject(jsonString); + JsonView jsonView = jsonObject.View(); + + PlainConfig::HttpProxyConfig httpProxyConfig; + httpProxyConfig.LoadFromJson(jsonView); + + ASSERT_FALSE(httpProxyConfig.httpProxyEnabled); +} + +TEST_F(ConfigTestFixture, HTTPProxyConfigNoAuth) +{ + constexpr char jsonString[] = R"( +{ + "http-proxy-enabled": true, + "http-proxy-host": "10.0.0.1", + "http-proxy-port": "8888", + "http-proxy-auth-method": "None" +})"; + JsonObject jsonObject(jsonString); + JsonView jsonView = jsonObject.View(); + + PlainConfig::HttpProxyConfig httpProxyConfig; + httpProxyConfig.LoadFromJson(jsonView); + + ASSERT_TRUE(httpProxyConfig.httpProxyEnabled); + ASSERT_STREQ("10.0.0.1", httpProxyConfig.proxyHost->c_str()); + ASSERT_EQ(8888, httpProxyConfig.proxyPort.value()); + ASSERT_FALSE(httpProxyConfig.httpProxyAuthEnabled); + ASSERT_STREQ("None", httpProxyConfig.proxyAuthMethod->c_str()); +} + TEST(Config, MemoryTrace) { PlainConfig config; diff --git a/test/util/TestProxyUtils.cpp b/test/util/TestProxyUtils.cpp new file mode 100644 index 00000000..825ada59 --- /dev/null +++ b/test/util/TestProxyUtils.cpp @@ -0,0 +1,88 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "../../source/util/ProxyUtils.h" +#include "gtest/gtest.h" + +using namespace Aws::Iot::DeviceClient; +using namespace Aws::Iot::DeviceClient::Util; + +class ProxyUtilsValidPortNumberParameterizedTestSuite : public ::testing::TestWithParam +{ +}; + +class ProxyUtilsInvalidPortNumberParameterizedTestSuite : public ::testing::TestWithParam +{ +}; + +class ProxyUtilsValidIPAddressParameterizedTestSuite : public ::testing::TestWithParam +{ +}; + +class ProxyUtilsInvalidIPAddressParameterizedTestSuite : public ::testing::TestWithParam +{ +}; + +TEST_P(ProxyUtilsValidPortNumberParameterizedTestSuite, ValidPortNumberTest) +{ + int portNumber = GetParam(); + ASSERT_TRUE(ProxyUtils::ValidatePortNumber(portNumber)); +} + +INSTANTIATE_TEST_SUITE_P( + ProxyUtilsValidPortNumberTest, + ProxyUtilsValidPortNumberParameterizedTestSuite, + ::testing::Values(1, 65535)); + +TEST_P(ProxyUtilsInvalidPortNumberParameterizedTestSuite, InvalidPortNumberTest) +{ + int portNumber = GetParam(); + ASSERT_FALSE(ProxyUtils::ValidatePortNumber(portNumber)); +} + +INSTANTIATE_TEST_SUITE_P( + ProxyUtilsValidPortNumberTest, + ProxyUtilsInvalidPortNumberParameterizedTestSuite, + ::testing::Values(-1, 65536, -65535, 0)); + +TEST_P(ProxyUtilsValidIPAddressParameterizedTestSuite, ValidIPAddressTest) +{ + std::string IPAddress = GetParam(); + ASSERT_TRUE(ProxyUtils::ValidateHostIpAddress(IPAddress)); +} + +INSTANTIATE_TEST_SUITE_P( + ProxyUtilsValidIPAddressTest, + ProxyUtilsValidIPAddressParameterizedTestSuite, + ::testing::Values( + "10.0.0.0", + "10.0.0.255", + "10.255.255.255", + "172.16.0.0", + "172.24.0.255", + "172.31.255.255", + "192.168.0.0", + "192.168.1.1", + "192.168.255.255")); + +TEST_P(ProxyUtilsInvalidIPAddressParameterizedTestSuite, ValidIPAddressTest) +{ + std::string IPAddress = GetParam(); + ASSERT_FALSE(ProxyUtils::ValidateHostIpAddress(IPAddress)); +} + +INSTANTIATE_TEST_SUITE_P( + ProxyUtilsValidIPAddressTest, + ProxyUtilsInvalidIPAddressParameterizedTestSuite, + ::testing::Values( + "10.0.01", + "8.8.8.8", + "amazon.com", + "9.255.255.255", + "11.0.0.0", + "172.15.255.255", + "172.32.0.0", + "192.167.255.255", + "192.169.0.0", + "255.255.255.255", + "999.999.999.999")); \ No newline at end of file