diff --git a/CMakeLists.txt b/CMakeLists.txt index e9a382733..86d6da615 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -418,6 +418,7 @@ if(NOT CMAKE_CROSSCOMPILING) add_subdirectory(bin/elasticurl_cpp) add_subdirectory(bin/mqtt5_app) add_subdirectory(bin/mqtt5_canary) + add_subdirectory(bin/mqtt5_socks5_app) endif() endif() endif() diff --git a/bin/elasticurl_cpp/main.cpp b/bin/elasticurl_cpp/main.cpp index 81ba074a0..2f31fffbf 100644 --- a/bin/elasticurl_cpp/main.cpp +++ b/bin/elasticurl_cpp/main.cpp @@ -6,6 +6,8 @@ #include #include #include + +#include #include #include @@ -40,11 +42,52 @@ struct ElasticurlCtx std::shared_ptr InputBody = nullptr; std::ofstream Output; + + // SOCKS5 proxy support + Aws::Crt::String ProxyHost; + uint16_t ProxyPort = 0; + bool UseProxy = false; + Aws::Crt::Optional Socks5ProxyOptions; }; -static void s_Usage(int exit_code) +// Parse SOCKS5 proxy URI and fill ElasticurlCtx fields +static bool s_ParseProxyUri(ElasticurlCtx &ctx, const char *proxy_arg) { + if (!proxy_arg || proxy_arg[0] == '\0') + { + std::cerr << "Proxy URI must not be empty" << std::endl; + return false; + } + ByteCursor uri_cursor = aws_byte_cursor_from_c_str(proxy_arg); + Io::Uri parsed_uri(uri_cursor, ctx.allocator); + if (!parsed_uri) + { + std::cerr << "Failed to parse proxy URI \"" << proxy_arg + << "\": " << aws_error_debug_str(parsed_uri.LastError()) << std::endl; + return false; + } + auto proxyOptions = Io::Socks5ProxyOptions::CreateFromUri(parsed_uri, ctx.ConnectTimeout, ctx.allocator); + if (!proxyOptions) + { + std::cerr << "Failed to create SOCKS5 proxy options from \"" << proxy_arg + << "\": " << aws_error_debug_str(Aws::Crt::LastError()) << std::endl; + return false; + } + ctx.Socks5ProxyOptions = *proxyOptions; + ByteCursor host_cursor = parsed_uri.GetHostName(); + ctx.ProxyHost.assign(reinterpret_cast(host_cursor.ptr), host_cursor.len); + uint32_t port = parsed_uri.GetPort(); + if (port == 0) + { + port = 1080; + } + ctx.ProxyPort = static_cast(port); + ctx.UseProxy = true; + return true; +} +static void s_Usage(int exit_code) +{ std::cerr << "usage: elasticurl [options] url\n"; std::cerr << " url: url to make a request to. The default is a GET request.\n"; std::cerr << "\n Options:\n\n"; @@ -65,6 +108,7 @@ static void s_Usage(int exit_code) std::cerr << " -o, --output FILE: dumps content-body to FILE instead of stdout.\n"; std::cerr << " -t, --trace FILE: dumps logs to FILE instead of stderr.\n"; std::cerr << " -v, --verbose: ERROR|INFO|DEBUG|TRACE: log level to configure. Default is none.\n"; + std::cerr << " --proxy URL: SOCKS5 proxy URI (socks5h://[user[:pass]@]host[:port]).\n"; std::cerr << " --version: print the version of elasticurl.\n"; std::cerr << " --http2: HTTP/2 connection required\n"; std::cerr << " --http1_1: HTTP/1.1 connection required\n"; @@ -91,6 +135,7 @@ static struct aws_cli_option s_LongOptions[] = { {"output", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, nullptr, 'o'}, {"trace", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, nullptr, 't'}, {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, nullptr, 'v'}, + {"proxy", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, nullptr, 'X'}, {"version", AWS_CLI_OPTIONS_NO_ARGUMENT, nullptr, 'V'}, {"http2", AWS_CLI_OPTIONS_NO_ARGUMENT, nullptr, 'w'}, {"http1_1", AWS_CLI_OPTIONS_NO_ARGUMENT, nullptr, 'W'}, @@ -104,7 +149,7 @@ static void s_ParseOptions(int argc, char **argv, ElasticurlCtx &ctx) while (true) { int option_index = 0; - int c = aws_cli_getopt_long(argc, argv, "a:b:c:e:f:H:d:g:M:GPHiko:t:v:VwWh", s_LongOptions, &option_index); + int c = aws_cli_getopt_long(argc, argv, "a:b:c:e:f:H:d:g:M:GPHiko:t:v:VwWhX:", s_LongOptions, &option_index); if (c == -1) { /* finished parsing */ @@ -126,6 +171,12 @@ static void s_ParseOptions(int argc, char **argv, ElasticurlCtx &ctx) s_Usage(1); } break; + case 'X': + if (!s_ParseProxyUri(ctx, aws_cli_optarg)) + { + s_Usage(1); + } + break; case 'a': ctx.CaCert = aws_cli_optarg; break; @@ -375,28 +426,30 @@ int main(int argc, char **argv) std::promise shutdownPromise; auto onConnectionSetup = - [&appCtx, &connectionPromise](const std::shared_ptr &newConnection, int errorCode) { - if (!errorCode) + [&appCtx, &connectionPromise](const std::shared_ptr &newConnection, int errorCode) + { + if (!errorCode) + { + if (appCtx.RequiredHttpVersion != Http::HttpVersion::Unknown) { - if (appCtx.RequiredHttpVersion != Http::HttpVersion::Unknown) + if (newConnection->GetVersion() != appCtx.RequiredHttpVersion) { - if (newConnection->GetVersion() != appCtx.RequiredHttpVersion) - { - std::cerr << "Error. The requested HTTP version, " << appCtx.Alpn - << ", is not supported by the peer." << std::endl; - exit(1); - } + std::cerr << "Error. The requested HTTP version, " << appCtx.Alpn + << ", is not supported by the peer." << std::endl; + exit(1); } } - else - { - std::cerr << "Connection failed with error " << aws_error_debug_str(errorCode) << std::endl; - exit(1); - } - connectionPromise.set_value(newConnection); - }; + } + else + { + std::cerr << "Connection failed with error " << aws_error_debug_str(errorCode) << std::endl; + exit(1); + } + connectionPromise.set_value(newConnection); + }; - auto onConnectionShutdown = [&shutdownPromise](Http::HttpClientConnection &newConnection, int errorCode) { + auto onConnectionShutdown = [&shutdownPromise](Http::HttpClientConnection &newConnection, int errorCode) + { (void)newConnection; if (errorCode) { @@ -418,6 +471,10 @@ int main(int argc, char **argv) } httpClientConnectionOptions.HostName = String((const char *)hostName.ptr, hostName.len); httpClientConnectionOptions.Port = port; + if (appCtx.UseProxy && appCtx.Socks5ProxyOptions.has_value()) + { + httpClientConnectionOptions.Socks5ProxyOptions = appCtx.Socks5ProxyOptions.value(); + } Http::HttpClientConnection::CreateConnection(httpClientConnectionOptions, allocator); @@ -430,7 +487,8 @@ int main(int argc, char **argv) requestOptions.request = &request; std::promise streamCompletePromise; - requestOptions.onStreamComplete = [&streamCompletePromise](Http::HttpStream &stream, int errorCode) { + requestOptions.onStreamComplete = [&streamCompletePromise](Http::HttpStream &stream, int errorCode) + { (void)stream; if (errorCode) { @@ -443,7 +501,8 @@ int main(int argc, char **argv) requestOptions.onIncomingHeaders = [&](Http::HttpStream &stream, enum aws_http_header_block header_block, const Http::HttpHeader *header, - std::size_t len) { + std::size_t len) + { /* Ignore informational headers */ if (header_block == AWS_HTTP_HEADER_BLOCK_INFORMATIONAL) { @@ -468,7 +527,8 @@ int main(int argc, char **argv) } } }; - requestOptions.onIncomingBody = [&appCtx](Http::HttpStream &, const ByteCursor &data) { + requestOptions.onIncomingBody = [&appCtx](Http::HttpStream &, const ByteCursor &data) + { if (appCtx.Output.is_open()) { appCtx.Output.write((char *)data.ptr, data.len); @@ -481,9 +541,12 @@ int main(int argc, char **argv) request.SetMethod(ByteCursorFromCString(appCtx.verb)); auto pathAndQuery = appCtx.uri.GetPathAndQuery(); - if (pathAndQuery.len > 0) { + if (pathAndQuery.len > 0) + { request.SetPath(pathAndQuery); - } else { + } + else + { request.SetPath(ByteCursorFromCString("/")); } diff --git a/bin/mqtt5_app/main.cpp b/bin/mqtt5_app/main.cpp index d099d8919..31f6216d8 100644 --- a/bin/mqtt5_app/main.cpp +++ b/bin/mqtt5_app/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -40,8 +41,54 @@ struct app_ctx const char *TraceFile; Aws::Crt::LogLevel LogLevel; + + Aws::Crt::String proxy_host; + uint16_t proxy_port; + bool use_proxy = false; + Aws::Crt::Optional socks5_proxy_options; }; +static bool s_parse_proxy_uri(struct app_ctx &ctx, const char *proxy_arg) +{ + if (!proxy_arg || proxy_arg[0] == '\0') + { + std::cerr << "Proxy URI must not be empty" << std::endl; + return false; + } + + ByteCursor uri_cursor = aws_byte_cursor_from_c_str(proxy_arg); + Io::Uri parsed_uri(uri_cursor, ctx.allocator); + if (!parsed_uri) + { + std::cerr << "Failed to parse proxy URI \"" << proxy_arg + << "\": " << aws_error_debug_str(parsed_uri.LastError()) << std::endl; + return false; + } + + auto proxyOptions = Io::Socks5ProxyOptions::CreateFromUri(parsed_uri, ctx.connect_timeout, ctx.allocator); + if (!proxyOptions) + { + std::cerr << "Failed to create SOCKS5 proxy options from \"" << proxy_arg + << "\": " << aws_error_debug_str(Aws::Crt::LastError()) << std::endl; + return false; + } + + ctx.socks5_proxy_options = *proxyOptions; + + ByteCursor host_cursor = parsed_uri.GetHostName(); + ctx.proxy_host.assign(reinterpret_cast(host_cursor.ptr), host_cursor.len); + + uint32_t port = parsed_uri.GetPort(); + if (port == 0) + { + port = 1080; + } + ctx.proxy_port = static_cast(port); + + ctx.use_proxy = true; + return true; +} + static void s_usage(int exit_code) { @@ -53,6 +100,7 @@ static void s_usage(int exit_code) fprintf(stderr, " --key FILE: Path to a PEM encoded private key that matches cert.\n"); fprintf(stderr, " -l, --log FILE: dumps logs to FILE instead of stderr.\n"); fprintf(stderr, " -v, --verbose: ERROR|INFO|DEBUG|TRACE: log level to configure. Default is none.\n"); + fprintf(stderr, " --proxy URL: SOCKS5 proxy URI (socks5h://[user[:pass]@]host[:port]).\n"); fprintf(stderr, " -h, --help\n"); fprintf(stderr, " Display this message and quit.\n"); @@ -66,6 +114,7 @@ static struct aws_cli_option s_long_options[] = { {"connect-timeout", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'f'}, {"log", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'l'}, {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'}, + {"proxy", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'X'}, {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, /* Per getopt(3) the last element of the array has to be filled with all zeros */ {NULL, AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 0}, @@ -76,7 +125,7 @@ static void s_parse_options(int argc, char **argv, struct app_ctx &ctx) while (true) { int option_index = 0; - int c = aws_cli_getopt_long(argc, argv, "a:b:c:e:f:H:d:g:M:GPHiko:t:v:VwWh", s_long_options, &option_index); + int c = aws_cli_getopt_long(argc, argv, "a:b:c:e:f:H:d:g:M:GPHiko:t:v:VwWhX:", s_long_options, &option_index); if (c == -1) { /* finished parsing */ @@ -130,6 +179,12 @@ static void s_parse_options(int argc, char **argv, struct app_ctx &ctx) } break; } + case 'X': + if (!s_parse_proxy_uri(ctx, aws_cli_optarg)) + { + s_usage(1); + } + break; default: std::cerr << "Unknown option\n"; s_usage(1); @@ -314,6 +369,26 @@ int main(int argc, char **argv) mqtt5OptionsBuilder.WithTlsConnectionOptions(tlsConnectionOptions); } + if (app_ctx.use_proxy && app_ctx.socks5_proxy_options && !app_ctx.proxy_host.empty()) + { + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Using SOCKS5 proxy " << app_ctx.proxy_host << ":" << app_ctx.proxy_port << std::endl; + const auto &proxy_opts = *app_ctx.socks5_proxy_options; + Aws::Crt::String username, password; + if (proxy_opts.GetUsername().has_value()) + { + username = proxy_opts.GetUsername().value(); + std::cout << "MQTT5: Proxy username: " << username << std::endl; + } + if (proxy_opts.GetPassword().has_value()) + { + password = proxy_opts.GetPassword().value(); + std::cout << "MQTT5: Proxy password: " << password << std::endl; + } + mqtt5OptionsBuilder.WithSocks5ProxyOptions(proxy_opts); + std::cout << "**********************************************************" << std::endl; + } + std::promise connectionPromise; std::promise disconnectionPromise; std::promise stoppedPromise; @@ -364,13 +439,14 @@ int main(int argc, char **argv) else { std::cout << "**********************************************************" << std::endl; - std::cout << "MQTT5:DisConnection failed with error " << aws_error_debug_str(eventData.errorCode) << std::endl; + std::cout << "MQTT5:DisConnection failed with error " << aws_error_debug_str(eventData.errorCode) + << std::endl; if (eventData.disconnectPacket != NULL) { if (eventData.disconnectPacket->getReasonString().has_value()) { - std::cout << "disconnect packet: " << eventData.disconnectPacket->getReasonString().value().c_str() - << std::endl; + std::cout << "disconnect packet: " + << eventData.disconnectPacket->getReasonString().value().c_str() << std::endl; } } std::cout << "**********************************************************" << std::endl; @@ -451,7 +527,8 @@ int main(int argc, char **argv) subscribe, [](int, std::shared_ptr packet) { - if(packet == nullptr) return; + if (packet == nullptr) + return; std::cout << "**********************************************************" << std::endl; std::cout << "MQTT5: check suback packet : " << std::endl; for (auto code : packet->getReasonCodes()) diff --git a/bin/mqtt5_canary/main.cpp b/bin/mqtt5_canary/main.cpp index cdbd690b4..4e79f7b63 100644 --- a/bin/mqtt5_canary/main.cpp +++ b/bin/mqtt5_canary/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -47,8 +48,54 @@ struct AppCtx const char *TraceFile; Aws::Crt::LogLevel LogLevel; + + Aws::Crt::String proxy_host; + uint16_t proxy_port; + bool use_proxy = false; + Aws::Crt::Optional socks5_proxy_options; }; +static bool s_parse_proxy_uri(AppCtx &ctx, const char *proxy_arg) +{ + if (!proxy_arg || proxy_arg[0] == '\0') + { + std::cerr << "Proxy URI must not be empty" << std::endl; + return false; + } + + ByteCursor uri_cursor = aws_byte_cursor_from_c_str(proxy_arg); + Io::Uri parsed_uri(uri_cursor, ctx.allocator); + if (!parsed_uri) + { + std::cerr << "Failed to parse proxy URI \"" << proxy_arg + << "\": " << aws_error_debug_str(parsed_uri.LastError()) << std::endl; + return false; + } + + auto proxyOptions = Io::Socks5ProxyOptions::CreateFromUri(parsed_uri, ctx.connect_timeout, ctx.allocator); + if (!proxyOptions) + { + std::cerr << "Failed to create SOCKS5 proxy options from \"" << proxy_arg + << "\": " << aws_error_debug_str(Aws::Crt::LastError()) << std::endl; + return false; + } + + ctx.socks5_proxy_options = *proxyOptions; + + ByteCursor host_cursor = parsed_uri.GetHostName(); + ctx.proxy_host.assign(reinterpret_cast(host_cursor.ptr), host_cursor.len); + + uint32_t port = parsed_uri.GetPort(); + if (port == 0) + { + port = 1080; + } + ctx.proxy_port = static_cast(port); + + ctx.use_proxy = true; + return true; +} + enum AwsMqtt5CanaryOperations { AWS_MQTT5_CANARY_OPERATION_NULL = 0, @@ -92,7 +139,7 @@ static void s_Usage(int exit_code) fprintf(stderr, " -l, --log FILE: dumps logs to FILE instead of stderr.\n"); fprintf(stderr, " -v, --verbose: ERROR|INFO|DEBUG|TRACE: log level to configure. Default is none.\n"); fprintf(stderr, " -w, --websockets: use mqtt-over-websockets rather than direct mqtt\n"); - fprintf(stderr, " -u, --tls: use tls with mqtt connection\n"); + fprintf(stderr, " --proxy URL: SOCKS5 proxy URI (socks5h://[user[:pass]@]host[:port]).\n"); fprintf(stderr, " -t, --threads: number of eventloop group threads to use\n"); fprintf(stderr, " -C, --clients: number of mqtt5 clients to use\n"); @@ -111,6 +158,7 @@ static struct aws_cli_option s_long_options[] = { {"log", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'l'}, {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'}, {"websockets", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'w'}, + {"proxy", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'X'}, {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, {"threads", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 't'}, @@ -123,11 +171,10 @@ static struct aws_cli_option s_long_options[] = { static void s_ParseOptions(int argc, char **argv, struct AppCtx &ctx, struct AwsMqtt5CanaryTesterOptions *testerOptions) { - while (true) { int option_index = 0; - int c = aws_cli_getopt_long(argc, argv, "a:c:e:f:l:v:wht:C:T:s:", s_long_options, &option_index); + int c = aws_cli_getopt_long(argc, argv, "a:c:e:f:l:v:wht:C:T:s:X:", s_long_options, &option_index); if (c == -1) { /* finished parsing */ @@ -160,9 +207,6 @@ static void s_ParseOptions(int argc, char **argv, struct AppCtx &ctx, struct Aws case 'w': ctx.use_websockets = true; break; - case 'u': - ctx.use_tls = true; - break; case 't': testerOptions->elgMaxThreads = (uint16_t)atoi(aws_cli_optarg); break; @@ -202,6 +246,12 @@ static void s_ParseOptions(int argc, char **argv, struct AppCtx &ctx, struct Aws case 's': testerOptions->testRunSeconds = atoi(aws_cli_optarg); break; + case 'X': + if (!s_parse_proxy_uri(ctx, aws_cli_optarg)) + { + s_Usage(1); + } + break; case 0x02: /* getopt_long() returns 0x02 (START_OF_TEXT) if a positional arg was encountered */ ctx.uri = Io::Uri(aws_byte_cursor_from_c_str(aws_cli_positional_arg), ctx.allocator); @@ -445,19 +495,22 @@ static int s_AwsMqtt5CanaryOperationSubscribe(struct AwsMqtt5CanaryTestClient *t ++g_statistic.subscribe_attempt; AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "ID:%s Subscribe to topic: %s", testClient->clientId.c_str(), topicArray); - if (testClient->client->Subscribe(packet, [](int errorcode, std::shared_ptr) { - if (errorcode != 0) + if (testClient->client->Subscribe( + packet, + [](int errorcode, std::shared_ptr) { - ++g_statistic.subscribe_failed; - AWS_LOGF_ERROR( - AWS_LS_MQTT5_CANARY, - "Subscribe failed with errorcode: %d, %s\n", - errorcode, - aws_error_str(errorcode)); - return; - } - ++g_statistic.subscribe_succeed; - })) + if (errorcode != 0) + { + ++g_statistic.subscribe_failed; + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CANARY, + "Subscribe failed with errorcode: %d, %s\n", + errorcode, + aws_error_str(errorcode)); + return; + } + ++g_statistic.subscribe_succeed; + })) { return AWS_OP_SUCCESS; } @@ -486,7 +539,9 @@ static int s_AwsMqtt5CanaryOperationUnsubscribeBad(struct AwsMqtt5CanaryTestClie ++g_statistic.totalOperations; ++g_statistic.unsub_attempt; if (testClient->client->Unsubscribe( - unsubscription, [testClient](int, std::shared_ptr packet) { + unsubscription, + [testClient](int, std::shared_ptr packet) + { if (packet == nullptr) return; if (packet->getReasonCodes()[0] == AWS_MQTT5_UARC_SUCCESS) @@ -589,20 +644,23 @@ static int s_AwsMqtt5CanaryOperationPublish( ++g_statistic.totalOperations; ++g_statistic.publish_attempt; - if (testClient->client->Publish(packetPublish, [testClient](int errorcode, std::shared_ptr ) { - if (errorcode != 0) + if (testClient->client->Publish( + packetPublish, + [testClient](int errorcode, std::shared_ptr) { - ++g_statistic.publish_failed; - AWS_LOGF_ERROR( - AWS_LS_MQTT5_CANARY, - "ID: %s Publish failed with error code: %d, %s\n", - testClient->clientId.c_str(), - errorcode, - aws_error_str(errorcode)); - return; - } - ++g_statistic.publish_succeed; - })) + if (errorcode != 0) + { + ++g_statistic.publish_failed; + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CANARY, + "ID: %s Publish failed with error code: %d, %s\n", + testClient->clientId.c_str(), + errorcode, + aws_error_str(errorcode)); + return; + } + ++g_statistic.publish_succeed; + })) { AWS_LOGF_INFO( AWS_LS_MQTT5_CANARY, "ID:%s Publish to topic %s", testClient->clientId.c_str(), topicFilter.c_str()); @@ -903,6 +961,25 @@ int main(int argc, char **argv) mqtt5Options.WithTlsConnectionOptions(tlsConnectionOptions); } + if (appCtx.use_proxy && appCtx.socks5_proxy_options && !appCtx.proxy_host.empty()) + { + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, "MQTT5: Using SOCKS5 proxy %s:%d", appCtx.proxy_host.c_str(), appCtx.proxy_port); + const auto &proxy_opts = *appCtx.socks5_proxy_options; + Aws::Crt::String username, password; + if (proxy_opts.GetUsername().has_value()) + { + username = proxy_opts.GetUsername().value(); + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "MQTT5: Proxy username: %s", username.c_str()); + } + if (proxy_opts.GetPassword().has_value()) + { + password = proxy_opts.GetPassword().value(); + AWS_LOGF_INFO(AWS_LS_MQTT5_CANARY, "MQTT5: Proxy password: %s", password.c_str()); + } + mqtt5Options.WithSocks5ProxyOptions(proxy_opts); + } + if (appCtx.use_websockets) { mqtt5Options.WithWebsocketHandshakeTransformCallback(s_AwsMqtt5TransformWebsocketHandshakeFn); @@ -926,16 +1003,19 @@ int main(int argc, char **argv) client.isConnected = false; clients.push_back(client); mqtt5Options.WithAckTimeoutSeconds(10); - mqtt5Options.WithPublishReceivedCallback([&clients, i](const Mqtt5::PublishReceivedEventData &publishData) { - AWS_LOGF_INFO( - AWS_LS_MQTT5_CANARY, - "Client:%s Publish Received on topic %s", - clients[i].clientId.c_str(), - publishData.publishPacket->getTopic().c_str()); - }); + mqtt5Options.WithPublishReceivedCallback( + [&clients, i](const Mqtt5::PublishReceivedEventData &publishData) + { + AWS_LOGF_INFO( + AWS_LS_MQTT5_CANARY, + "Client:%s Publish Received on topic %s", + clients[i].clientId.c_str(), + publishData.publishPacket->getTopic().c_str()); + }); mqtt5Options.WithClientConnectionSuccessCallback( - [&clients, i](const Mqtt5::OnConnectionSuccessEventData &eventData) { + [&clients, i](const Mqtt5::OnConnectionSuccessEventData &eventData) + { clients[i].isConnected = true; clients[i].clientId = Aws::Crt::String( eventData.negotiatedSettings->getClientId().c_str(), @@ -947,7 +1027,8 @@ int main(int argc, char **argv) }); mqtt5Options.WithClientConnectionFailureCallback( - [&clients, i](const OnConnectionFailureEventData &eventData) { + [&clients, i](const OnConnectionFailureEventData &eventData) + { clients[i].isConnected = false; AWS_LOGF_ERROR( AWS_LS_MQTT5_CANARY, @@ -957,15 +1038,20 @@ int main(int argc, char **argv) aws_error_debug_str(eventData.errorCode)); }); - mqtt5Options.WithClientDisconnectionCallback([&clients, i](const OnDisconnectionEventData &) { - clients[i].isConnected = false; - AWS_LOGF_ERROR(AWS_LS_MQTT5_CANARY, "ID:%s Lifecycle Event: Disconnect", clients[i].clientId.c_str()); - }); + mqtt5Options.WithClientDisconnectionCallback( + [&clients, i](const OnDisconnectionEventData &) + { + clients[i].isConnected = false; + AWS_LOGF_ERROR( + AWS_LS_MQTT5_CANARY, "ID:%s Lifecycle Event: Disconnect", clients[i].clientId.c_str()); + }); - mqtt5Options.WithClientStoppedCallback([&clients, i](const OnStoppedEventData &) { - clients[i].isConnected = false; - AWS_LOGF_ERROR(AWS_LS_MQTT5_CANARY, "ID:%s Lifecycle Event: Stopped", clients[i].clientId.c_str()); - }); + mqtt5Options.WithClientStoppedCallback( + [&clients, i](const OnStoppedEventData &) + { + clients[i].isConnected = false; + AWS_LOGF_ERROR(AWS_LS_MQTT5_CANARY, "ID:%s Lifecycle Event: Stopped", clients[i].clientId.c_str()); + }); clients[i].client = Mqtt5::Mqtt5Client::NewMqtt5Client(mqtt5Options, appCtx.allocator); if (clients[i].client == nullptr) diff --git a/bin/mqtt5_socks5_app/CMakeLists.txt b/bin/mqtt5_socks5_app/CMakeLists.txt new file mode 100644 index 000000000..56741c1e1 --- /dev/null +++ b/bin/mqtt5_socks5_app/CMakeLists.txt @@ -0,0 +1,47 @@ +project(mqtt5_socks5_app CXX) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_PREFIX_PATH}/lib/cmake") + +file(GLOB mqtt5_socks5_app + "*.cpp" + ) + +set(MQTT_SOCKS5_APP_PROJECT_NAME mqtt5_socks5_app) +add_executable(${MQTT_SOCKS5_APP_PROJECT_NAME} ${mqtt5_socks5_app}) + +aws_add_sanitizers(${MQTT_SOCKS5_APP_PROJECT_NAME}) + +set_target_properties(${MQTT_SOCKS5_APP_PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX) +set_target_properties(${MQTT_SOCKS5_APP_PROJECT_NAME} PROPERTIES CXX_STANDARD ${CMAKE_CXX_STANDARD}) + +#set warnings and runtime library +if (MSVC) + if(AWS_STATIC_MSVC_RUNTIME_LIBRARY OR STATIC_CRT) + target_compile_options(${MQTT_SOCKS5_APP_PROJECT_NAME} PRIVATE "/MT$<$:d>") + else() + target_compile_options(${MQTT_SOCKS5_APP_PROJECT_NAME} PRIVATE "/MD$<$:d>") + endif() + target_compile_options(${MQTT_SOCKS5_APP_PROJECT_NAME} PRIVATE /W4 /WX) +else () + target_compile_options(${MQTT_SOCKS5_APP_PROJECT_NAME} PRIVATE -Wall -Wno-long-long -pedantic -Werror) +endif () + + +target_compile_definitions(${MQTT_SOCKS5_APP_PROJECT_NAME} PRIVATE $<$:DEBUG_BUILD>) + +target_include_directories(${MQTT_SOCKS5_APP_PROJECT_NAME} PUBLIC + $ + $) + +target_link_libraries(${MQTT_SOCKS5_APP_PROJECT_NAME} PRIVATE aws-crt-cpp) + +if (BUILD_SHARED_LIBS AND NOT WIN32) + message(INFO " mqtt5 socks5 app will be built with shared libs, but you may need to set LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib to run the application") +endif() + +install(TARGETS ${MQTT_SOCKS5_APP_PROJECT_NAME} + EXPORT ${MQTT_SOCKS5_APP_PROJECT_NAME}-targets + COMPONENT Runtime + RUNTIME + DESTINATION bin + COMPONENT Runtime) diff --git a/bin/mqtt5_socks5_app/main.cpp b/bin/mqtt5_socks5_app/main.cpp new file mode 100644 index 000000000..acf17ac50 --- /dev/null +++ b/bin/mqtt5_socks5_app/main.cpp @@ -0,0 +1,823 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Aws::Crt; +using namespace Aws::Crt::Mqtt5; + +enum class CredentialsProviderSource { + DefaultChain, + Environment, + Profile, + Static +}; + +struct app_ctx +{ + Allocator *allocator = nullptr; + Io::Uri uri; + uint32_t port = 0; + const char *cacert = nullptr; + const char *cert = nullptr; + const char *key = nullptr; + int connect_timeout = 0; + + aws_tls_connection_options tls_connection_options; + + const char *TraceFile = nullptr; + Aws::Crt::LogLevel LogLevel = Aws::Crt::LogLevel::None; + + Aws::Crt::String proxy_host_storage; + uint16_t proxy_port = 0; + bool use_proxy = false; + Aws::Crt::Optional socks5_proxy_options; + + bool enable_tls = false; + bool use_websocket = false; + Aws::Crt::String region; + CredentialsProviderSource credentials_source = CredentialsProviderSource::DefaultChain; + Aws::Crt::String profile_name; + Aws::Crt::String config_file; + Aws::Crt::String credentials_file; + Aws::Crt::String access_key_id; + Aws::Crt::String secret_access_key; + Aws::Crt::String session_token; + bool port_overridden = false; + bool use_ipv6 = false; +}; + +static bool s_parse_proxy_uri(app_ctx &ctx, const char *proxy_arg) +{ + if (!proxy_arg || proxy_arg[0] == '\0') + { + std::cerr << "Proxy URI must not be empty\n"; + return false; + } + + ByteCursor uri_cursor = aws_byte_cursor_from_c_str(proxy_arg); + Io::Uri parsed_uri(uri_cursor, ctx.allocator); + if (!parsed_uri) + { + std::cerr << "Failed to parse proxy URI \"" << proxy_arg + << "\": " << aws_error_debug_str(parsed_uri.LastError()) << std::endl; + return false; + } + + auto proxyOptions = Io::Socks5ProxyOptions::CreateFromUri(parsed_uri, 10000, ctx.allocator); + if (!proxyOptions) + { + std::cerr << "Failed to create SOCKS5 proxy options from \"" << proxy_arg + << "\": " << aws_error_debug_str(Aws::Crt::LastError()) << std::endl; + return false; + } + + ctx.socks5_proxy_options = *proxyOptions; + + ByteCursor host_cursor = parsed_uri.GetHostName(); + ctx.proxy_host_storage.assign(reinterpret_cast(host_cursor.ptr), host_cursor.len); + + uint32_t port = parsed_uri.GetPort(); + if (port == 0) + { + port = 1080; + } + ctx.proxy_port = static_cast(port); + + ctx.use_proxy = true; + return true; +} + +static void s_usage(int exit_code) +{ + fprintf(stderr, "usage: mqtt_socks5_cpp_example [options]\n"); + fprintf(stderr, " --broker-host HOST: MQTT broker hostname (default: test.mosquitto.org)\n"); + fprintf(stderr, " --broker-port PORT: MQTT broker port (default: 1883 for MQTT, 8883 for MQTTS)\n"); + fprintf(stderr, " --proxy URL: SOCKS5 proxy URI (socks5h://... for proxy DNS, socks5://... for local DNS)\n"); + fprintf(stderr, " --cert FILE: Client certificate file path (PEM format)\n"); + fprintf(stderr, " --key FILE: Private key file path (PEM format)\n"); + fprintf(stderr, " --ca-file FILE: CA certificate file path (PEM format)\n"); + fprintf(stderr, " --websocket: Use MQTT over WebSocket with SigV4 authentication\n"); + fprintf(stderr, " --region REGION: AWS Region for SigV4 signing when using WebSocket\n"); + fprintf(stderr, + " --credential-source SOURCE: Credentials provider source (default-chain, environment, profile, static)\n"); + fprintf(stderr, " --profile NAME: AWS profile to use when credential source is profile\n"); + fprintf(stderr, " --config-file PATH: AWS config file override for profile credential source\n"); + fprintf(stderr, " --credentials-file PATH: AWS credentials file override for profile credential source\n"); + fprintf(stderr, " --access-key KEY: AWS access key for static credential source\n"); + fprintf(stderr, " --secret-key KEY: AWS secret access key for static credential source\n"); + fprintf(stderr, " --session-token TOKEN: AWS session token for static credential source (optional)\n"); + fprintf(stderr, " --ipv6: Force IPv6 socket domain\n"); + fprintf(stderr, " --verbose: Print detailed logging\n"); + fprintf(stderr, " --help: Display this message and exit\n"); + exit(exit_code); +} + +static struct aws_cli_option s_long_options[] = { + {"broker-host", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'b'}, + {"broker-port", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'p'}, + {"proxy", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'x'}, + {"cert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'C'}, + {"key", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'K'}, + {"ca-file", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'A'}, + {"websocket", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'W'}, + {"ipv6", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, '6'}, + {"region", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'R'}, + {"credential-source", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'S'}, + {"profile", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'P'}, + {"config-file", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'F'}, + {"credentials-file", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'G'}, + {"access-key", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'I'}, + {"secret-key", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'J'}, + {"session-token", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'T'}, + {"verbose", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'v'}, + {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, + {NULL, AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 0}, // Ensure proper termination +}; + +static void s_parse_options(int argc, char **argv, struct app_ctx &ctx) +{ + ctx.use_proxy = false; + ctx.proxy_host_storage.clear(); + ctx.proxy_port = 0; + ctx.socks5_proxy_options.reset(); + + while (true) + { + int option_index = 0; + int c = aws_cli_getopt_long(argc, argv, "b:p:x:C:K:A:W6R:S:P:F:G:I:J:T:vh", s_long_options, &option_index); + if (c == -1) + { + break; + } + + switch (c) + { + case 'b': + ctx.uri = Io::Uri(aws_byte_cursor_from_c_str(aws_cli_optarg), ctx.allocator); + break; + case 'p': + ctx.port = static_cast(atoi(aws_cli_optarg)); + ctx.port_overridden = true; + break; + case 'x': + if (!s_parse_proxy_uri(ctx, aws_cli_optarg)) + { + s_usage(1); + } + break; + case 'C': + ctx.cert = aws_cli_optarg; + break; + case 'K': + ctx.key = aws_cli_optarg; + break; + case 'A': + ctx.cacert = aws_cli_optarg; + break; + case 'W': + ctx.use_websocket = true; + break; + case '6': + ctx.use_ipv6 = true; + break; + case 'R': + ctx.region = aws_cli_optarg; + break; + case 'S': { + Aws::Crt::String source = aws_cli_optarg; + std::transform(source.begin(), source.end(), source.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + if (source == "default-chain") { + ctx.credentials_source = CredentialsProviderSource::DefaultChain; + } else if (source == "environment") { + ctx.credentials_source = CredentialsProviderSource::Environment; + } else if (source == "profile") { + ctx.credentials_source = CredentialsProviderSource::Profile; + } else if (source == "static") { + ctx.credentials_source = CredentialsProviderSource::Static; + } else { + std::cerr << "Unknown credential source '" << aws_cli_optarg + << "'. Expected one of: default-chain, environment, profile, static." << std::endl; + s_usage(1); + } + break; + } + case 'P': + ctx.profile_name = aws_cli_optarg; + break; + case 'F': + ctx.config_file = aws_cli_optarg; + break; + case 'G': + ctx.credentials_file = aws_cli_optarg; + break; + case 'I': + ctx.access_key_id = aws_cli_optarg; + break; + case 'J': + ctx.secret_access_key = aws_cli_optarg; + break; + case 'T': + ctx.session_token = aws_cli_optarg; + break; + case 'v': + ctx.LogLevel = Aws::Crt::LogLevel::Trace; + break; + case 'h': + s_usage(0); + break; + default: + std::cerr << "Unknown option\n"; + s_usage(1); + } + } + + if (ctx.use_websocket) + { + ctx.enable_tls = true; + if (!ctx.port_overridden && ctx.port == 1883 && !ctx.uri.GetPort()) + { + ctx.port = 443; + } + } + if (!ctx.enable_tls) { + ctx.enable_tls = ctx.cacert || ctx.cert || ctx.key; + } +} + +/********************************************************** + * MAIN + **********************************************************/ + +/** + * This example demonstrates basic MQTT5 client functionality using SOCKS5 proxy and optional TLS/WebSocket. + * + * It is primarily used for integration tests to validate end-to-end connectivity and message flow + * with different combinations of proxy, TLS, and WebSocket options. + * + * The workflow for the application is: + * 1. Connect to the MQTT broker (optionally via SOCKS5 proxy, TLS, and/or WebSocket). + * 2. Subscribe to the topic "test/topic/test1" with QoS 1. + * 3. Publish the message "mqtt5 publish test" to "test/topic/test1". + * 4. Wait to receive the published message back on the subscribed topic. + * 5. Disconnect from the broker and exit. + * + * This example does not require user interaction and does not demonstrate multiple subscriptions or unsubscriptions. + * It is intended as a minimal end-to-end test of connect, subscribe, publish, receive, and disconnect using various + * connection options. + */ + +void PrintAppOptions(const app_ctx &ctx) +{ + std::cout << "================= MQTT5 SOCKS5 APP OPTIONS =================" << std::endl; + Aws::Crt::String hostNameStr = + Aws::Crt::String((const char *)ctx.uri.GetHostName().ptr, ctx.uri.GetHostName().len); + std::cout << "Broker Host: " << hostNameStr << std::endl; + std::cout << "Broker Port: " << ctx.port << std::endl; + std::cout << "TLS Enabled: " << (ctx.enable_tls ? "yes" : "no") << std::endl; + if (ctx.cacert) std::cout << "CA Cert: " << ctx.cacert << std::endl; + if (ctx.cert && !ctx.use_websocket) std::cout << "Client Cert: " << ctx.cert << std::endl; + if (ctx.key && !ctx.use_websocket) std::cout << "Client Key: " << ctx.key << std::endl; + std::cout << "Connect Timeout (ms): " << ctx.connect_timeout << std::endl; + if (ctx.use_websocket) { + std::cout << "Using WebSocket: yes" << std::endl; + if (!ctx.region.empty()) { + std::cout << "AWS Region: " << ctx.region << std::endl; + } + std::cout << "Credentials Source: "; + switch (ctx.credentials_source) { + case CredentialsProviderSource::DefaultChain: + std::cout << "default-chain"; + break; + case CredentialsProviderSource::Environment: + std::cout << "environment"; + break; + case CredentialsProviderSource::Profile: + std::cout << "profile"; + if (!ctx.profile_name.empty()) { + std::cout << " (profile=" << ctx.profile_name << ")"; + } + if (!ctx.config_file.empty()) { + std::cout << " (config-file=" << ctx.config_file << ")"; + } + if (!ctx.credentials_file.empty()) { + std::cout << " (credentials-file=" << ctx.credentials_file << ")"; + } + break; + case CredentialsProviderSource::Static: + std::cout << "static"; + if (!ctx.access_key_id.empty()) { + std::cout << " (access-key provided)"; + } + if (!ctx.session_token.empty()) { + std::cout << " (session token provided)"; + } + break; + } + std::cout << std::endl; + } else { + std::cout << "Using WebSocket: no" << std::endl; + } + std::cout << "Socket Domain: " << (ctx.use_ipv6 ? "IPv6" : "IPv4") << std::endl; + if (ctx.use_proxy && ctx.socks5_proxy_options && !ctx.proxy_host_storage.empty()) { + std::cout << "SOCKS5 Proxy Host: " << ctx.proxy_host_storage << std::endl; + std::cout << "SOCKS5 Proxy Port: " << ctx.proxy_port << std::endl; + bool resolveViaProxy = + ctx.socks5_proxy_options->GetHostResolutionMode() == Io::AwsSocks5HostResolutionMode::Proxy; + std::cout << "SOCKS5 DNS Resolution: " << (resolveViaProxy ? "proxy" : "client") << std::endl; + const aws_socks5_proxy_options *rawProxyOptions = ctx.socks5_proxy_options->GetUnderlyingHandle(); + if (rawProxyOptions->username && rawProxyOptions->password) + { + std::cout << "SOCKS5 Proxy Auth: username='" << aws_string_c_str(rawProxyOptions->username) + << "', password=***" << std::endl; + } + else + { + std::cout << "SOCKS5 Proxy Auth: none" << std::endl; + } + } + else + { + std::cout << "SOCKS5 Proxy: not configured" << std::endl; + } + std::cout << "============================================================" << std::endl; +} + +int main(int argc, char **argv) +{ + + struct aws_allocator *allocator = aws_mem_tracer_new(aws_default_allocator(), NULL, AWS_MEMTRACE_STACKS, 15); + + struct app_ctx app_ctx = {}; + app_ctx.allocator = allocator; + app_ctx.connect_timeout = 3000; + app_ctx.port = 1883; + + s_parse_options(argc, argv, app_ctx); + if (app_ctx.uri.GetPort()) + { + app_ctx.port = app_ctx.uri.GetPort(); + } + + if (app_ctx.use_websocket) + { + if (app_ctx.region.empty()) + { + std::cerr << "[ERROR] --region must be specified when using --websocket for SigV4 authentication." + << std::endl; + return 1; + } + + if (app_ctx.credentials_source == CredentialsProviderSource::Static && + (app_ctx.access_key_id.empty() || app_ctx.secret_access_key.empty())) + { + std::cerr << "[ERROR] Static credentials require both --access-key and --secret-key when using WebSocket." + << std::endl; + return 1; + } + + if (app_ctx.cert || app_ctx.key) + { + std::cout << "[INFO] Client certificate and key are ignored when using WebSocket SigV4 authentication." + << std::endl; + } + } + + /********************************************************** + * LOGGING + **********************************************************/ + + ApiHandle apiHandle(allocator); + if (app_ctx.TraceFile) + { + apiHandle.InitializeLogging(app_ctx.LogLevel, app_ctx.TraceFile); + } + else + { + apiHandle.InitializeLogging(app_ctx.LogLevel, stderr); + } + + bool useTls = app_ctx.use_websocket || app_ctx.enable_tls; + + auto hostName = app_ctx.uri.GetHostName(); + + /*************************************************** + * setup connection configs + ***************************************************/ + + Io::TlsContextOptions tlsCtxOptions; + Io::TlsContext tlsContext; + Io::TlsConnectionOptions tlsConnectionOptions; + if (useTls) + { + if (app_ctx.use_websocket) + { + std::cout << "MQTT5: Configuring TLS for WebSocket connection with SigV4 authentication." << std::endl; + tlsCtxOptions = Io::TlsContextOptions::InitDefaultClient(); + if (!tlsCtxOptions) + { + std::cout << "Failed to create TLS options for WebSocket with error " + << aws_error_debug_str(tlsCtxOptions.LastError()) << std::endl; + exit(1); + } + } + else if (app_ctx.cert && app_ctx.key) + { + std::cout << "MQTT5: Configuring TLS with cert " << app_ctx.cert << " and key " << app_ctx.key << std::endl; + tlsCtxOptions = Io::TlsContextOptions::InitClientWithMtls(app_ctx.cert, app_ctx.key); + if (!tlsCtxOptions) + { + std::cout << "Failed to load " << app_ctx.cert << " and " << app_ctx.key << " with error " + << aws_error_debug_str(tlsCtxOptions.LastError()) << std::endl; + exit(1); + } + } + else + { + std::cout << "MQTT5: Configuring TLS with default settings." << std::endl; + tlsCtxOptions = Io::TlsContextOptions::InitDefaultClient(); + if (!tlsCtxOptions) + { + std::cout << "Failed to create a default tlsCtxOptions with error " + << aws_error_debug_str(tlsCtxOptions.LastError()) << std::endl; + exit(1); + } + } + + if (app_ctx.cacert) + { + std::cout << "MQTT5: Configuring TLS with CA " << app_ctx.cacert << std::endl; + tlsCtxOptions.OverrideDefaultTrustStore(nullptr, app_ctx.cacert); + } + tlsContext = Io::TlsContext(tlsCtxOptions, Io::TlsMode::CLIENT, allocator); + + tlsConnectionOptions = tlsContext.NewConnectionOptions(); + + std::cout << "MQTT5: Looking into the uri string: " + << static_cast(AWS_BYTE_CURSOR_PRI(app_ctx.uri.GetFullUri())) << std::endl; + + if (!tlsConnectionOptions.SetServerName(hostName)) + { + std::cout << "Failed to set servername with error " << aws_error_debug_str(tlsConnectionOptions.LastError()) + << std::endl; + exit(1); + } + } + + Io::SocketOptions socketOptions; + socketOptions.SetConnectTimeoutMs(app_ctx.connect_timeout); + socketOptions.SetKeepAliveIntervalSec(0); + socketOptions.SetKeepAlive(false); + socketOptions.SetKeepAliveTimeoutSec(0); + socketOptions.SetSocketDomain(app_ctx.use_ipv6 ? Io::SocketDomain::IPv6 : Io::SocketDomain::IPv4); + + Io::EventLoopGroup eventLoopGroup(0, allocator); + if (!eventLoopGroup) + { + std::cerr << "Failed to create evenloop group with error " << aws_error_debug_str(eventLoopGroup.LastError()) + << std::endl; + exit(1); + } + + Io::DefaultHostResolver defaultHostResolver(eventLoopGroup, 8, 30, allocator); + if (!defaultHostResolver) + { + std::cerr << "Failed to create host resolver with error " + << aws_error_debug_str(defaultHostResolver.LastError()) << std::endl; + exit(1); + } + + Io::ClientBootstrap clientBootstrap(eventLoopGroup, defaultHostResolver, allocator); + if (!clientBootstrap) + { + std::cerr << "Failed to create client bootstrap with error " << aws_error_debug_str(clientBootstrap.LastError()) + << std::endl; + exit(1); + } + clientBootstrap.EnableBlockingShutdown(); + + PrintAppOptions(app_ctx); + + /********************************************************** + * MQTT5 CLIENT CREATION + **********************************************************/ + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Start ConnectPacket...." << std::endl; + std::cout << "**********************************************************" << std::endl; + std::shared_ptr packet_connect = std::make_shared(); + packet_connect->WithReceiveMaximum(9); + packet_connect->WithMaximumPacketSizeBytes(128 * 1024); + + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Start Option Builder...." << std::endl; + std::cout << "**********************************************************" << std::endl; + Aws::Crt::String namestring((const char *)hostName.ptr, hostName.len); + Aws::Crt::Mqtt5::Mqtt5ClientOptions mqtt5OptionsBuilder(app_ctx.allocator); + mqtt5OptionsBuilder.WithHostName(namestring).WithPort(app_ctx.port); + + mqtt5OptionsBuilder.WithConnectOptions(packet_connect) + .WithSocketOptions(socketOptions) + .WithBootstrap(&clientBootstrap); + + if (useTls) + { + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Configuring TLS...." << std::endl; + std::cout << "**********************************************************" << std::endl; + mqtt5OptionsBuilder.WithTlsConnectionOptions(tlsConnectionOptions); + } + + // Configure WebSocket if requested + std::shared_ptr websocketCredentialsProvider; + if (app_ctx.use_websocket) { + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Configuring WebSocket with SigV4 authentication...." << std::endl; + std::cout << "**********************************************************" << std::endl; + + Aws::Iot::WebsocketConfig websocketConfig(app_ctx.region, &clientBootstrap, app_ctx.allocator); + + switch (app_ctx.credentials_source) { + case CredentialsProviderSource::DefaultChain: + // Already handled by the default constructor using the client bootstrap. + break; + case CredentialsProviderSource::Environment: + websocketCredentialsProvider = + Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderEnvironment(app_ctx.allocator); + break; + case CredentialsProviderSource::Profile: { + Aws::Crt::Auth::CredentialsProviderProfileConfig profileConfig; + profileConfig.Bootstrap = &clientBootstrap; + if (!app_ctx.profile_name.empty()) { + profileConfig.ProfileNameOverride = aws_byte_cursor_from_c_str(app_ctx.profile_name.c_str()); + } + if (!app_ctx.config_file.empty()) { + profileConfig.ConfigFileNameOverride = + aws_byte_cursor_from_c_str(app_ctx.config_file.c_str()); + } + if (!app_ctx.credentials_file.empty()) { + profileConfig.CredentialsFileNameOverride = + aws_byte_cursor_from_c_str(app_ctx.credentials_file.c_str()); + } + websocketCredentialsProvider = + Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(profileConfig, app_ctx.allocator); + break; + } + case CredentialsProviderSource::Static: { + Aws::Crt::Auth::CredentialsProviderStaticConfig staticConfig; + staticConfig.AccessKeyId = aws_byte_cursor_from_c_str(app_ctx.access_key_id.c_str()); + staticConfig.SecretAccessKey = aws_byte_cursor_from_c_str(app_ctx.secret_access_key.c_str()); + if (!app_ctx.session_token.empty()) { + staticConfig.SessionToken = aws_byte_cursor_from_c_str(app_ctx.session_token.c_str()); + } + websocketCredentialsProvider = + Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderStatic(staticConfig, app_ctx.allocator); + break; + } + } + + if (app_ctx.credentials_source != CredentialsProviderSource::DefaultChain) { + if (!websocketCredentialsProvider) { + std::cerr << "[ERROR] Failed to create credentials provider for WebSocket connection." << std::endl; + return 1; + } + websocketConfig = Aws::Iot::WebsocketConfig(app_ctx.region, websocketCredentialsProvider, app_ctx.allocator); + } + + auto websocketConfigShared = std::make_shared(websocketConfig); + + mqtt5OptionsBuilder.WithWebsocketHandshakeTransformCallback( + [websocketConfigShared](std::shared_ptr req, + const Aws::Crt::Mqtt5::OnWebSocketHandshakeInterceptComplete &onComplete) { + auto signingComplete = [onComplete]( + const std::shared_ptr &signedRequest, + int errorCode) { onComplete(signedRequest, errorCode); }; + auto signerConfig = websocketConfigShared->CreateSigningConfigCb(); + websocketConfigShared->Signer->SignRequest(req, *signerConfig, signingComplete); + }); + } + + if (app_ctx.use_proxy && app_ctx.socks5_proxy_options && !app_ctx.proxy_host_storage.empty()) + { + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Configuring SOCKS5 Proxy with host " << app_ctx.proxy_host_storage << " and port " + << app_ctx.proxy_port << std::endl; + bool resolveViaProxy = + app_ctx.socks5_proxy_options->GetHostResolutionMode() == Io::AwsSocks5HostResolutionMode::Proxy; + std::cout << "MQTT5: SOCKS5 DNS mode: " << (resolveViaProxy ? "proxy-resolved" : "client-resolved") + << std::endl; + + const aws_socks5_proxy_options *rawProxyOptions = app_ctx.socks5_proxy_options->GetUnderlyingHandle(); + if (rawProxyOptions->username && rawProxyOptions->password) + { + std::cout << "MQTT5: Configuring SOCKS5 Proxy with username " << aws_string_c_str(rawProxyOptions->username) + << " and password ***" << std::endl; + } + else + { + std::cout << "MQTT5: Configuring SOCKS5 Proxy with no authentication." << std::endl; + } + + mqtt5OptionsBuilder.WithSocks5ProxyOptions(*app_ctx.socks5_proxy_options); + } + else + { + std::cout << "No SOCKS5 proxy configured." << std::endl; + } + + std::promise connectionPromise; + std::promise stoppedPromise; + std::promise publishReceivedPromise; + + mqtt5OptionsBuilder.WithClientConnectionSuccessCallback( + [&connectionPromise](const OnConnectionSuccessEventData &eventData) + { + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5:Connected:: " << eventData.negotiatedSettings->getClientId().c_str() << std::endl; + std::cout << "**********************************************************" << std::endl; + connectionPromise.set_value(true); + }); + + mqtt5OptionsBuilder.WithClientConnectionFailureCallback( + [&connectionPromise](const OnConnectionFailureEventData &eventData) + { + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5:Connection failed with error " << aws_error_debug_str(eventData.errorCode) << std::endl; + std::cout << "**********************************************************" << std::endl; + connectionPromise.set_value(false); + }); + + mqtt5OptionsBuilder.WithClientStoppedCallback( + [&stoppedPromise](const OnStoppedEventData &) + { + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5:client stopped." << std::endl; + std::cout << "**********************************************************" << std::endl; + stoppedPromise.set_value(); + }); + + mqtt5OptionsBuilder.WithPublishReceivedCallback( + [&publishReceivedPromise](const PublishReceivedEventData &eventData) + { + ByteCursor payload = eventData.publishPacket->getPayload(); + String msg = String((const char *)payload.ptr, payload.len); + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5:Received Message: " << msg.c_str() << std::endl; + std::cout << "**********************************************************" << std::endl; + if (msg == "mqtt5 publish test") + { + publishReceivedPromise.set_value(); + } + }); + + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Start Init Client ...." << std::endl; + auto mqtt5Client = Aws::Crt::Mqtt5::Mqtt5Client::NewMqtt5Client(mqtt5OptionsBuilder, app_ctx.allocator); + + if (mqtt5Client == nullptr) + { + std::cerr << "Failed to Init Mqtt5Client with error code: %d." << ErrorDebugString(LastError()) << std::endl; + return -1; + } + + std::cout << "MQTT5: Finish Init Client ...." << std::endl; + std::cout << "**********************************************************" << std::endl; + + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Client Start ...." << std::endl; + std::cout << "**********************************************************" << std::endl; + + if (!(mqtt5Client->Start() && connectionPromise.get_future().get() == true)) + { + std::cout << "[ERROR]Failed to start the client " << std::endl; + return 1; // Connection failure + } + + // Subscribe to a single topic + Mqtt5::Subscription sub(app_ctx.allocator); + sub.WithTopicFilter("test/topic/test1").WithQOS(Mqtt5::QOS::AWS_MQTT5_QOS_AT_LEAST_ONCE); + Vector subscriptionList; + subscriptionList.push_back(sub); + std::shared_ptr subscribe = std::make_shared(app_ctx.allocator); + subscribe->WithSubscriptions(subscriptionList); + + std::promise subscribeAckPromise; + bool subscribeSuccess = mqtt5Client->Subscribe( + subscribe, + [&subscribeAckPromise](int errorCode, std::shared_ptr packet) + { + if (errorCode != AWS_ERROR_SUCCESS || packet == nullptr) { + subscribeAckPromise.set_value(false); + return; + } + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: check suback packet : " << std::endl; + bool allGranted = true; + for (auto code : packet->getReasonCodes()) + { + std::cout << "Get suback with codes: " << code << std::endl; + if (code > 2) { // Non-granted reason codes are >= 0x80 + allGranted = false; + } + } + std::cout << "**********************************************************" << std::endl; + subscribeAckPromise.set_value(allGranted); + }); + + if (!subscribeSuccess) + { + std::cout << "[ERROR]Subscription Failed." << std::endl; + if (mqtt5Client->Stop()) + { + stoppedPromise.get_future().get(); + } + return 2; // Subscription failure + } + + // Wait for SUBACK before publishing so we don't race the proxy latency. + std::cout << "**********************************************************" << std::endl; + std::cout << "MQTT5: Waiting for SUBACK confirmation..." << std::endl; + std::cout << "**********************************************************" << std::endl; + bool subAckGranted = false; + try { + subAckGranted = subscribeAckPromise.get_future().get(); + } catch (...) { + subAckGranted = false; + } + if (!subAckGranted) { + std::cout << "[ERROR]Subscription was not granted by broker." << std::endl; + mqtt5Client->Stop(); + stoppedPromise.get_future().get(); + return 2; + } + + // Publish to the same topic + ByteCursor payload = Aws::Crt::ByteCursorFromCString("mqtt5 publish test"); + std::shared_ptr publish = std::make_shared(app_ctx.allocator); + publish->WithTopic("test/topic/test1"); + publish->WithPayload(payload); + publish->WithQOS(Mqtt5::QOS::AWS_MQTT5_QOS_AT_LEAST_ONCE); + + std::cout << "**********************************************************" << std::endl; + std::cout << "Publish Start:" << std::endl; + std::cout << "**********************************************************" << std::endl; + if (!mqtt5Client->Publish(publish)) + { + std::cout << "**********************************************************" << std::endl; + std::cout << "[ERROR]Publish Failed." << std::endl; + std::cout << "**********************************************************" << std::endl; + mqtt5Client->Stop(); + stoppedPromise.get_future().get(); + return 3; // Publish failure + } + + std::cout << "**********************************************************" << std::endl; + std::cout << "Mqtt5: Waiting for published message..." << std::endl; + std::cout << "**********************************************************" << std::endl; + try + { + publishReceivedPromise.get_future().get(); + } + catch (...) + { + std::cout << "[ERROR]Did not receive published message." << std::endl; + mqtt5Client->Stop(); + stoppedPromise.get_future().get(); + return 4; // Message not received + } + + // Disconnect + if (mqtt5Client->Stop()) + { + stoppedPromise.get_future().get(); + } + else + { + std::cout << "[ERROR]Failed to stop the client " << std::endl; + return 5; // Disconnect failure + } + return 0; +} diff --git a/docsrc/SOCKS5_Proxy_Support.md b/docsrc/SOCKS5_Proxy_Support.md new file mode 100644 index 000000000..f78beda42 --- /dev/null +++ b/docsrc/SOCKS5_Proxy_Support.md @@ -0,0 +1,161 @@ +# SOCKS5 Proxy Support in AWS CRT + +This document describes how SOCKS5 proxy support is built into the AWS Common Runtime (CRT), how it integrates with the existing channel handler architecture, and how to configure and consume SOCKS5 features from both the C and C++ APIs. + +Note: This document is intended to guide you through the changes introduced in this pull request. You may remove this document after the PR has been reviewed and merged. + +## Implementation Overview + +SOCKS5 support is implemented as a channel handler (`aws_socks5_channel_handler`) that sits between the transport socket and higher-level protocol handlers. The handler drives the SOCKS5 handshake, authenticates with the proxy if required, and then becomes a transparent pass-through once the connection to the ultimate destination is established. + +- `aws_client_bootstrap_new_socket_channel_with_socks5()` wraps the standard bootstrap flow to insert the SOCKS5 handler and, if needed, defer TLS until the proxy negotiation completes (see `crt/aws-c-io/source/socks5_channel_handler.c`). +- The handler uses `aws_socks5_context` utilities from `crt/aws-c-io/include/aws/io/socks5.h` to format and parse the protocol messages (greeting, optional auth, connect, reply). +- After a successful handshake the handler signals completion via the original bootstrap callbacks. If TLS is requested, the handler chains into `aws_tls_channel_handler` so that the TLS handshake runs over the proxy tunnel before handing control back to the caller. + +## Channel Handler Lifecycle + +The SOCKS5 channel handler maintains its own state machine (`AWS_SOCKS5_CHANNEL_STATE_*`) that maps onto the protocol’s phases: + +1. **INIT** – handler created; waits for socket slot assignment. +2. **GREETING** – sends the client greeting listing supported auth methods and waits for the proxy’s selection. +3. **AUTH** – if username/password is required, sends credentials and validates the response. +4. **CONNECT** – sends the CONNECT request for the original destination, waits for the proxy’s reply. +5. **ESTABLISHED** – handshake succeeds; handler switches to transparent forwarding. +6. **ERROR** – any failure surfaces through the original setup callback with the captured error code. + +Timeouts during the handshake are enforced via `connect_timeout_ms` from the proxy options (converted to nanoseconds and scheduled as a channel task). When the timeout fires the handler tears down the channel and reports `AWS_IO_SOCKET_TIMEOUT`. + +## Bootstrap Integration + +`aws_client_bootstrap_new_socket_channel_with_socks5()` reuses the caller’s channel options but rewrites the socket target to hit the proxy instead of the end host. Internally it: + +1. Deep-copies the caller’s `aws_socks5_proxy_options`. +2. Sets the proxy target (`host`, `port`) as the TCP destination for the socket. +3. Captures the original destination endpoint (host and port) before rewiring the socket to the proxy so the handler can issue the CONNECT request on the caller’s behalf. +4. Captures TLS configuration so the TLS handler can be inserted after the SOCKS5 handshake, preserving the user’s negotiation callback and user data. +5. Installs the SOCKS5 handler as an intermediate channel slot before optionally chaining in TLS and higher-level protocol handlers. + +During shutdown the adapter restores the original callbacks and cleans up the copied proxy/TLS options to avoid leaking allocations. + +## Configuration Options + +`struct aws_socks5_proxy_options` (declared in `crt/aws-c-io/include/aws/io/socks5.h`) contains all configuration needed by the proxy handler: + +| Field | Description | +| ----- | ----------- | +| `host`, `port` | SOCKS5 proxy endpoint (owned strings copied by init helpers). | +| `username`, `password` | Optional credentials. If both are set, username/password auth is negotiated. | +| `connection_timeout_ms` | Milliseconds to wait for the full SOCKS5 negotiation before timing out. | +| `host_resolution_mode` | Controls whether the destination hostname is resolved by the proxy (`PROXY`) or by the local client (`CLIENT`). Defaults to the proxy behaviour for backwards compatibility. | + +The destination endpoint (host, port, and inferred address type) is captured when the SOCKS5 context is initialized or when the channel handler is constructed; applications no longer set it directly on the proxy options. + +Helper functions: + +- `aws_socks5_proxy_options_init()` / `_init_default()` allocate and populate proxy host/port. +- `aws_socks5_proxy_options_set_auth()` copies username/password credentials (RFC 1929 limits apply). +- `aws_socks5_proxy_options_set_host_resolution_mode()` switches between proxy-resolved (`AWS_SOCKS5_HOST_RESOLUTION_PROXY`) and client-resolved (`AWS_SOCKS5_HOST_RESOLUTION_CLIENT`) destinations. Use client resolution when the proxy cannot resolve the target hostname or when your application needs to pin specific IPs. +- Destination host/port are supplied when initializing the SOCKS5 context; no extra helper is required on the proxy options. +- `aws_socks5_proxy_options_copy()` and `_clean_up()` provide deep-copy semantics used by the bootstrap layer and language bindings. + +### C++ Wrapper (`Aws::Crt::Io::Socks5ProxyOptions`) + +The C++ wrapper in `include/aws/crt/io/Socks5ProxyOptions.h` owns an `aws_socks5_proxy_options` instance and exposes the same configuration through setters and RAII semantics. It defaults to the process allocator and ensures copies are deep copies while moves transfer ownership. Helpers (`SetHostResolutionMode()` / `GetHostResolutionMode()`) surface the host-resolution toggle via the strongly typed `AwsSocks5HostResolutionMode` enum. + + +CLI samples accept `socks5h://` URIs to keep the legacy “proxy-resolved DNS” behaviour, and now also recognise `socks5://` URIs when DNS resolution should stay on the client. The calculated scheme is translated directly into the C struct’s `host_resolution_mode`. + +## High-Level Client Integration + +SOCKS5 proxy integration is available to higher-level protocols through their respective connection options: + +- **HTTP** – `aws_http_client_connection_options::socks5_proxy_options` and `Aws::Crt::Http::HttpClientConnectionOptions::Socks5ProxyOptions` forward SOCKS5 configuration into the bootstrap path (`crt/aws-c-http/source/connection.c`). If both HTTP and SOCKS5 proxies are supplied, HTTP proxy options take precedence because the HTTP path must control the CONNECT tunnel yourself. +- **MQTT 3.1.1** – call `aws_mqtt_client_connection_set_socks5_proxy_options()` before `Connect()` (implemented in `crt/aws-c-mqtt/source/client.c`). The C++ `Aws::Crt::Mqtt::MqttConnection::SetSocks5ProxyOptions()` helper forwards the wrapper options. +- **MQTT5** – populate `aws_mqtt5_client_options::socks5_proxy_options` or call `Aws::Crt::Mqtt5::Mqtt5ClientOptions::WithSocks5ProxyOptions()`. During connection (`crt/aws-c-mqtt/source/v5/mqtt5_client.c`) SOCKS5 is preferred over HTTP proxies when both are set, mirroring MQTT3 behavior. + +Once the SOCKS5 handshake succeeds the rest of the protocol stack operates exactly as it would over a direct TCP connection. + +## Channel Handler Stack + +The channel is composed of handlers layered in order, regardless of protocol. The SOCKS5 handler negotiates the proxy before handing control to TLS (if enabled) and the final application protocol handler. + +```mermaid +flowchart LR + subgraph Channel ["Channel"] + direction LR + Socket["socket_handler"] + SOCKS5["socks5_handler"] + TLS["tls_handler"] + App["application_protocol_handler"] + + Socket --> SOCKS5 --> TLS --> App + end +``` + +Once negotiation completes the SOCKS5 handler becomes transparent, leaving the downstream handlers to operate as if the connection were direct. + +## C++ MQTT5 Usage Example + +```cpp +#include +#include +#include + +using namespace Aws::Crt; + +void ConnectThroughProxy() { + ApiHandle apiHandle; + + Io::Socks5ProxyOptions proxyOptions( + "proxy.internal.local", + 1080, + Io::Socks5ProxyAuthConfig::CreateUsernamePassword("alice", "s3cr3t!"), + 10000 /* timeout in ms */, + AwsSocks5HostResolutionMode::Proxy); + + Mqtt5::Mqtt5ClientOptions mqttOptions; + mqttOptions.WithHostName("broker.example.com") + .WithPort(8883) + .WithBootstrap(ApiHandle::GetOrCreateStaticDefaultClientBootstrap()) + .WithSocketOptions(Io::SocketOptions()) + .WithTlsConnectionOptions(Io::TlsConnectionOptions()) + .WithSocks5ProxyOptions(proxyOptions); + + auto client = Mqtt5::Mqtt5Client::Create(mqttOptions); + if (!client) { + fprintf(stderr, "Failed to create MQTT5 client: %s\n", ErrorDebugString(LastError())); + return; + } + + client->Start(); /* the client handles reconnects and lifecycle callbacks */ +} +``` + +Key points: + +- Configure proxy options *before* starting the client. +- Supply TLS settings as usual—the TLS handshake automatically runs after the SOCKS5 negotiation. +- `connection_timeout_ms` should cover the full round-trip to the proxy and destination handshake. +- When you already have a `socks5://` or `socks5h://` URI, call `Io::Socks5ProxyOptions::CreateFromUri()` to populate the options in one step (see the example applications for usage). + +## Best Practices and Notes + +- Enable TRACE logging for `AWS_LS_IO_SOCKS5` when debugging handshake issues—the handler logs state transitions and proxy replies. +- Username/password credentials are limited to 255 bytes each (RFC 1929). Longer values will fail validation in `aws_socks5_proxy_options_set_auth()`. +- Use `Socks5ProxyAuthConfig` with `SetAuth()` so the proxy configuration can adopt new authentication mechanisms without breaking your code. +- When both HTTP and SOCKS5 proxies are configured for MQTT the HTTP proxy wins, since the HTTP path needs to manage CONNECT tunnels explicitly. +- `aws_socks5_infer_address_type()` (used internally during context setup) determines whether the destination is IPv4, IPv6, or a hostname. Override the value manually only if you need non-standard routing. +- Remember to clean up `aws_socks5_proxy_options` with `aws_socks5_proxy_options_clean_up()` when using the C API directly. The C++ wrapper performs this automatically. + +## Comments for reviewers + +- **New binaries added for testing:** + - Binaries such as `mqtt5_socks5_app`, `http_client_app`, `mqtt5_client_app`, and `mqtt3_client_app` have been added for testing purposes. These can be removed or converted into integration tests if needed. +- **Example updates:** + - Existing example applications have been updated to set SOCKS5 options. These examples may contain boilerplate code and can be refactored for clarity and maintainability. +- **Integration test instructions:** + - Instructions for running integration tests have been added to the documentation. These instructions are temporary and can be removed once the PR has been reviewed and merged. +- **HTTP cleanup changes:** + - Some changes were made to address double cleanup when both SOCKS5 and HTTP channel handlers are notified about a connection failure. This issue was triggered by the MQTT canary test. The change should be double-checked, as it might be a false positive. +- **Proxy precedence:** + - If both HTTP proxy and SOCKS5 proxy options are set, SOCKS5 currently takes precedence. In some cases, this is detected and an error is reported. The logic for proxy precedence may need to be revisited to ensure all scenarios are handled correctly. diff --git a/docsrc/mqtt5_socks5_websocket.md b/docsrc/mqtt5_socks5_websocket.md new file mode 100644 index 000000000..73d3bbc02 --- /dev/null +++ b/docsrc/mqtt5_socks5_websocket.md @@ -0,0 +1,90 @@ +# MQTT5 SOCKS5 Example over WebSockets with IAM + +This document explains how to run the `mqtt5_socks5_app` sample against AWS IoT Core using MQTT5 over WebSockets with SigV4 (IAM) authentication. You can continue to layer SOCKS5 proxy connectivity on top of the WebSocket connection if needed. + +## Prerequisites + +- Build the sample as part of the standard `aws-crt-cpp` build: + + ```bash + cmake --build --target mqtt5_socks5_app + ``` + +- Ensure the executable `/bin/mqtt5_socks5_app` is available in your PATH or reference it by absolute path. +- Collect your AWS IoT endpoint (for example `abcdefghijklmnop-ats.iot.us-east-1.amazonaws.com`). +- Download the Amazon Root CA if your environment does not already trust it. You can pass it to the sample with `--ca-file /path/to/AmazonRootCA1.pem`. + +## New command line options + +When using WebSockets, the following options become relevant: + +- `--websocket` – enables the SigV4 WebSocket flow. +- `--region ` – AWS Region used to sign the WebSocket upgrade request. **Required** when `--websocket` is set. +- `--credential-source ` – selects the IAM credentials provider. Supported values: + - `default-chain` (default): cached chain of environment → profile → IMDS. + - `environment`: resolves credentials exclusively from environment variables. + - `profile`: resolves credentials from an AWS profile (use together with the overrides below). + - `static`: uses credentials supplied on the command line. +- `--profile ` – profile to use when `--credential-source profile`. +- `--config-file ` / `--credentials-file ` – optional overrides for profile resolution. +- `--access-key `, `--secret-key `, `--session-token ` – static credentials for `--credential-source static`. Session token is optional. + +When `--websocket` is enabled, the sample ignores any `--cert` or `--key` parameters because mTLS is not used for WebSocket/IAM authentication. + +## Supplying credentials + +### Default credential chain + +No extra options are required. The application constructs the default cached chain (environment → AWS profile → IMDS/ECS) using the same event loop group as the MQTT client. + +### Environment variables + +Export the standard environment variables and use `--credential-source environment`: + +```bash +export AWS_ACCESS_KEY_ID=AKIA... +export AWS_SECRET_ACCESS_KEY=... +export AWS_SESSION_TOKEN=... # optional + +mqtt5_socks5_app --broker-host --websocket \ + --region us-east-1 --credential-source environment +``` + +### AWS profile + +Use `--credential-source profile` plus optional overrides: + +```bash +mqtt5_socks5_app --broker-host --websocket \ + --region us-west-2 --credential-source profile \ + --profile iot-lab --config-file ~/.aws/config \ + --credentials-file ~/.aws/credentials +``` + +### Static credentials + +Provide the key material directly on the command line: + +```bash +mqtt5_socks5_app --broker-host --websocket \ + --region eu-central-1 --credential-source static \ + --access-key AKIA... --secret-key abcd1234... \ + --session-token FwoG... # optional +``` + +## SOCKS5 proxy usage + +SOCKS5 options remain available with WebSockets. For example: + +```bash +mqtt5_socks5_app --broker-host --websocket \ + --region us-east-1 --proxy socks5h://proxy.example.com:1080 +``` + +Proxy authentication and DNS resolution mode are automatically forwarded to the MQTT5 client. + +## Tips + +- The sample defaults to port `443` when `--websocket` is set and no explicit port is supplied. +- You can still provide `--ca-file` to trust a custom root CA. Client certificates are ignored during WebSocket/SigV4 flows. +- Use `--verbose` to enable trace logging if troubleshooting SigV4 signing or proxy connectivity. diff --git a/include/aws/crt/auth/Credentials.h b/include/aws/crt/auth/Credentials.h index 68aa62820..dbe0a79ae 100644 --- a/include/aws/crt/auth/Credentials.h +++ b/include/aws/crt/auth/Credentials.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -295,9 +296,14 @@ namespace Aws struct AWS_CRT_CPP_API CredentialsProviderX509Config { CredentialsProviderX509Config() - : Bootstrap(nullptr), TlsOptions(), ThingName(), RoleAlias(), Endpoint(), ProxyOptions() - { - } + : Bootstrap(nullptr), + TlsOptions(), + ThingName(), + RoleAlias(), + Endpoint(), + ProxyOptions(), + Socks5ProxyOptions() + {} /** * Connection bootstrap to use to create the http connection required to @@ -329,6 +335,11 @@ namespace Aws * (Optional) Http proxy configuration for the http request that fetches credentials */ Optional ProxyOptions; + + /** + * (Optional) SOCKS5 proxy configuration for the http request that fetches credentials + */ + Optional Socks5ProxyOptions; }; /** diff --git a/include/aws/crt/http/HttpConnection.h b/include/aws/crt/http/HttpConnection.h index 1b5c72053..7955132e7 100644 --- a/include/aws/crt/http/HttpConnection.h +++ b/include/aws/crt/http/HttpConnection.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -411,6 +412,12 @@ namespace Aws */ Optional ProxyOptions; + /** + * The SOCKS5 proxy options for the http connection. + * Optional. + */ + Optional Socks5ProxyOptions; + /** * If set to true, then the TCP read back pressure mechanism will be enabled. You should * only use this if you're allowing http response body data to escape the callbacks. E.g. you're @@ -510,5 +517,5 @@ namespace Aws }; } // namespace Http - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/include/aws/crt/io/Socks5ProxyOptions.h b/include/aws/crt/io/Socks5ProxyOptions.h new file mode 100644 index 000000000..752b1912a --- /dev/null +++ b/include/aws/crt/io/Socks5ProxyOptions.h @@ -0,0 +1,247 @@ +#pragma once +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include + +#include + +struct aws_socks5_proxy_options; + +namespace Aws +{ + namespace Crt + { + namespace Io + { + /** + * SOCKS5 authentication methods as defined in RFC 1928 + */ + enum class AwsSocks5AuthMethod + { + /** + * No authentication required + */ + None = 0x00, + + /** + * Username/password authentication (RFC 1929) + */ + UsernamePassword = 0x02, + + /** + * No acceptable methods (server response) + */ + NoAcceptableMethods = 0xFF + }; + + enum class AwsSocks5HostResolutionMode + { + Proxy = AWS_SOCKS5_HOST_RESOLUTION_PROXY, + Client = AWS_SOCKS5_HOST_RESOLUTION_CLIENT, + }; + + struct AWS_CRT_CPP_API Socks5ProxyAuthConfig + { + AwsSocks5AuthMethod Method{AwsSocks5AuthMethod::None}; + Optional Username; + Optional Password; + + static Socks5ProxyAuthConfig CreateNone() noexcept + { + return Socks5ProxyAuthConfig{}; + } + + static Socks5ProxyAuthConfig CreateUsernamePassword(const String &username, const String &password) + { + Socks5ProxyAuthConfig config; + config.Method = AwsSocks5AuthMethod::UsernamePassword; + config.Username = username; + config.Password = password; + return config; + } + }; + + /** + * Configuration structure that holds all SOCKS5 proxy-related connection options + */ + class AWS_CRT_CPP_API Socks5ProxyOptions + { + public: + /* Default SOCKS5 proxy port */ + static constexpr uint16_t DefaultProxyPort = 1080; + + Socks5ProxyOptions() noexcept; + + //TODO: remove deprecated constructor + Socks5ProxyOptions( + const String &hostName, + uint32_t port, + AwsSocks5AuthMethod authMethod, + const String &username, + const String &password, + uint32_t connectionTimeoutMs, + struct aws_allocator *allocator, + AwsSocks5HostResolutionMode resolutionMode = AwsSocks5HostResolutionMode::Proxy); + + Socks5ProxyOptions( + const String &hostName, + uint32_t port = DefaultProxyPort, + const Socks5ProxyAuthConfig &authConfig = Socks5ProxyAuthConfig::CreateNone(), + uint32_t connectionTimeoutMs = 0, + AwsSocks5HostResolutionMode resolutionMode = AwsSocks5HostResolutionMode::Proxy, + struct aws_allocator *allocator = aws_default_allocator()); + + + Socks5ProxyOptions(const Socks5ProxyOptions &rhs); + + Socks5ProxyOptions(Socks5ProxyOptions &&rhs) noexcept; + + Socks5ProxyOptions &operator=(const Socks5ProxyOptions &rhs); + Socks5ProxyOptions &operator=(Socks5ProxyOptions &&rhs) noexcept; + + ~Socks5ProxyOptions(); + + /** + * @return true when the proxy options contain a configured endpoint, false otherwise. + */ + explicit operator bool() const noexcept; + + /** + * @return the last aws error code encountered while operating on this instance. + */ + int LastError() const noexcept; + + /** + * Returns the underlying C struct for SOCKS5 proxy options. + */ + aws_socks5_proxy_options *GetUnderlyingHandle() { return &m_options; } + + /** + * Returns the underlying C struct for SOCKS5 proxy options (const). + */ + const aws_socks5_proxy_options *GetUnderlyingHandle() const { return &m_options; } + + /** + * Updates the SOCKS5 proxy endpoint configuration. + * + * @param hostName Proxy host name or address. + * @param port Proxy port. Must be <= UINT16_MAX. + * + * @return true when the endpoint was accepted, false otherwise with LastError() set. + */ + bool SetProxyEndpoint(const String &hostName, uint32_t port); + + /** + * Applies a SOCKS5 authentication configuration. + * + * @param authConfig Authentication configuration to apply. + * + * @return true on success, false on failure with LastError() set. + */ + bool SetAuth(const Socks5ProxyAuthConfig &authConfig); + + /** + * Sets username/password authentication for the SOCKS5 proxy. + * + * @param username User name for proxy authentication. Must be non-empty. + * @param password Password for proxy authentication. Must be non-empty. + * + * @return true on success, false on failure with LastError() set. + */ + bool SetAuthCredentials(const String &username, const String &password); + + /** + * Clears any previously configured authentication credentials. + */ + void ClearAuthCredentials(); + + /** + * Sets the host resolution mode (proxy/client) for the SOCKS5 proxy. + */ + void SetHostResolutionMode(AwsSocks5HostResolutionMode mode); + + /** + * Returns the host resolution mode (proxy/client) for the SOCKS5 proxy. + */ + AwsSocks5HostResolutionMode GetHostResolutionMode() const; + + /** + * Sets the connection timeout in milliseconds for the SOCKS5 proxy. + */ + void SetConnectionTimeoutMs(uint32_t timeoutMs); + + /** + * Returns the SOCKS5 proxy host as a string, or empty Optional if not set. + */ + Optional GetHost() const; + + /** + * Returns the SOCKS5 proxy port number. + */ + uint16_t GetPort() const; + + /** + * Returns the connection timeout in milliseconds for the SOCKS5 proxy. + */ + uint32_t GetConnectionTimeoutMs() const; + + /** + * Returns the authentication method used for the SOCKS5 proxy. + */ + AwsSocks5AuthMethod GetAuthMethod() const; + + /** + * Returns the SOCKS5 proxy username as a string, or empty Optional if not set. + */ + Optional GetUsername() const; + + /** + * Returns the SOCKS5 proxy password as a string, or empty Optional if not set. + */ + Optional GetPassword() const; + + /** + * Returns the host resolution mode (proxy/client) for the SOCKS5 proxy. + */ + AwsSocks5HostResolutionMode GetResolutionMode() const; + + /** + * Creates SOCKS5 proxy options from a parsed URI. The URI scheme must be socks5 or socks5h. + * Username and password are pulled from the authority userinfo when present (requires both + * username and password to enable authentication). socks5h implies proxy-side name resolution, + * socks5 implies client-side name resolution. + * + * Uri format: socks5[h]://[username:password@]host[:port] + * + * @param uri Parsed URI describing the proxy endpoint. + * @param connectionTimeoutMs Optional connection timeout in milliseconds applied to the proxy + * connection. + * @param allocator Allocator used for underlying allocations (defaults to the process allocator). + * + * @return Populated proxy options on success, otherwise an empty Optional with aws_last_error() set. + */ + static Optional CreateFromUri( + const Uri &uri, + uint32_t connectionTimeoutMs = 0, + struct aws_allocator *allocator = nullptr); + + private: + // Helper function to apply authentication configuration to aws_socks5_proxy_options struct + bool ApplyAuthConfig(aws_socks5_proxy_options &options, const Socks5ProxyAuthConfig &authConfig); + + aws_socks5_proxy_options m_options{}; + struct aws_allocator *m_allocator{nullptr}; + int m_lastError{0}; + Socks5ProxyAuthConfig m_authConfig{}; + + }; + + } // namespace Io + } // namespace Crt +} // namespace Aws diff --git a/include/aws/crt/mqtt/Mqtt5Client.h b/include/aws/crt/mqtt/Mqtt5Client.h index 82330bbbd..ab191d950 100644 --- a/include/aws/crt/mqtt/Mqtt5Client.h +++ b/include/aws/crt/mqtt/Mqtt5Client.h @@ -516,6 +516,15 @@ namespace Aws Mqtt5ClientOptions &WithHttpProxyOptions( const Crt::Http::HttpClientConnectionProxyOptions &proxyOptions) noexcept; + /** + * Sets the SOCKS5 proxy options for the MQTT client. + * + * @param proxyOptions The SOCKS5 proxy options to use. + * + * @return this option object + */ + Mqtt5ClientOptions &WithSocks5ProxyOptions(const Crt::Io::Socks5ProxyOptions &proxyOptions) noexcept; + /** * Sets mqtt5 connection options * @@ -780,6 +789,11 @@ namespace Aws */ Crt::Optional m_proxyOptions; + /** + * Optional SOCKS5 proxy options for the MQTT client. + */ + Crt::Optional m_socks5ProxyOptions; + /** * All configurable options with respect to the CONNECT packet sent by the client, including the will. * These connect properties will be used for every connection attempt made by the client. @@ -837,8 +851,9 @@ namespace Aws Crt::Allocator *m_allocator; aws_http_proxy_options m_httpProxyOptionsStorage; aws_mqtt5_packet_connect_view m_packetConnectViewStorage; + aws_socks5_proxy_options m_socks5ProxyOptionsStorage{}; }; } // namespace Mqtt5 - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/include/aws/crt/mqtt/MqttClient.h b/include/aws/crt/mqtt/MqttClient.h index 19b2236a3..d04ebbcc6 100644 --- a/include/aws/crt/mqtt/MqttClient.h +++ b/include/aws/crt/mqtt/MqttClient.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -117,5 +118,5 @@ namespace Aws aws_mqtt_client *m_client; }; } // namespace Mqtt - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/include/aws/crt/mqtt/MqttConnection.h b/include/aws/crt/mqtt/MqttConnection.h index cd8049425..2962e013f 100644 --- a/include/aws/crt/mqtt/MqttConnection.h +++ b/include/aws/crt/mqtt/MqttConnection.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -220,6 +221,12 @@ namespace Aws */ bool SetHttpProxyOptions(const Http::HttpClientConnectionProxyOptions &proxyOptions) noexcept; + /** + * Sets the SOCKS5 proxy options for this connection. + * This must be called before Connect(). + */ + bool SetSocks5ProxyOptions(const Aws::Crt::Io::Socks5ProxyOptions &options) noexcept; + /** * Customize time to wait between reconnect attempts. * The time will start at min and multiply by 2 until max is reached. @@ -458,5 +465,5 @@ namespace Aws std::shared_ptr m_connectionCore; }; } // namespace Mqtt - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/include/aws/crt/mqtt/private/MqttConnectionCore.h b/include/aws/crt/mqtt/private/MqttConnectionCore.h index 827cc9db4..e6ab38c7c 100644 --- a/include/aws/crt/mqtt/private/MqttConnectionCore.h +++ b/include/aws/crt/mqtt/private/MqttConnectionCore.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -103,6 +104,15 @@ namespace Aws */ bool SetHttpProxyOptions(const Http::HttpClientConnectionProxyOptions &proxyOptions) noexcept; + /** + * @internal + * Sets socks5 proxy options. + * @param proxyOptions proxy configuration for making the mqtt connection + * + * @return success/failure + */ + bool SetSocks5ProxyOptions(const Io::Socks5ProxyOptions &proxyOptions) noexcept; + /** * @internal * Customize time to wait between reconnect attempts. @@ -368,6 +378,7 @@ namespace Aws Io::TlsConnectionOptions m_tlsOptions; Io::SocketOptions m_socketOptions; Crt::Optional m_proxyOptions; + Crt::Optional m_socks5ProxyOptions; void *m_onAnyCbData; bool m_useTls; bool m_useWebsocket; @@ -390,6 +401,6 @@ namespace Aws std::shared_ptr m_self; }; } // namespace Mqtt - } // namespace Crt + } // namespace Crt } // namespace Aws /*! \endcond */ diff --git a/include/aws/iot/Mqtt5Client.h b/include/aws/iot/Mqtt5Client.h index b78f12822..6611df831 100644 --- a/include/aws/iot/Mqtt5Client.h +++ b/include/aws/iot/Mqtt5Client.h @@ -329,6 +329,15 @@ namespace Aws Mqtt5ClientBuilder &WithHttpProxyOptions( const Crt::Http::HttpClientConnectionProxyOptions &proxyOptions) noexcept; + /** + * Sets SOCKS5 proxy options. + * + * @param proxyOptions SOCKS5 proxy configuration for connection establishment + * + * @return this option object + */ + Mqtt5ClientBuilder &WithSocks5ProxyOptions(const Crt::Io::Socks5ProxyOptions &proxyOptions) noexcept; + /** * Sets the custom authorizer settings. This function will modify the username, port, and TLS options. * @@ -649,6 +658,11 @@ namespace Aws */ Crt::Optional m_proxyOptions; + /** + * Configures (tunneling) SOCKS5 proxy usage when establishing MQTT connections + */ + Crt::Optional m_socks5ProxyOptions; + /** * Websocket related options. The clinet with use websocket for connection when set. */ diff --git a/include/aws/iot/MqttClient.h b/include/aws/iot/MqttClient.h index 886e25a29..7e494d5c0 100644 --- a/include/aws/iot/MqttClient.h +++ b/include/aws/iot/MqttClient.h @@ -64,7 +64,9 @@ namespace Aws const Crt::Io::SocketOptions &socketOptions, Crt::Io::TlsContext &&tlsContext, Crt::Mqtt::OnWebSocketHandshakeIntercept &&interceptor, - const Crt::Optional &proxyOptions); + const Crt::Optional &proxyOptions, + const Crt::Optional &socks5ProxyOptions = + Crt::Optional()); /** * @return true if the instance is in a valid state, false otherwise. @@ -84,7 +86,9 @@ namespace Aws uint32_t port, const Crt::Io::SocketOptions &socketOptions, Crt::Io::TlsContext &&tlsContext, - const Crt::Optional &proxyOptions); + const Crt::Optional &proxyOptions, + const Crt::Optional &socks5ProxyOptions = + Crt::Optional()); Crt::String m_endpoint; uint32_t m_port; @@ -94,6 +98,7 @@ namespace Aws Crt::String m_username; Crt::String m_password; Crt::Optional m_proxyOptions; + Crt::Optional m_socks5ProxyOptions; int m_lastError; friend class MqttClient; @@ -319,6 +324,15 @@ namespace Aws MqttClientConnectionConfigBuilder &WithHttpProxyOptions( const Crt::Http::HttpClientConnectionProxyOptions &proxyOptions) noexcept; + /** + * Configures the connection to use a SOCKS5 proxy. This is mutually exclusive with HTTP proxy options. + */ + MqttClientConnectionConfigBuilder &WithSocks5ProxyOptions(const Crt::Io::Socks5ProxyOptions &proxyOptions) + { + m_socks5ProxyOptions = proxyOptions; + return *this; + } + /** * Whether to send the SDK name and version number in the MQTT CONNECT packet. * Default is True. @@ -454,6 +468,7 @@ namespace Aws Crt::Io::TlsContextOptions m_contextOptions; Crt::Optional m_websocketConfig; Crt::Optional m_proxyOptions; + Crt::Optional m_socks5ProxyOptions; bool m_enableMetricsCollection = true; Crt::String m_sdkName = "CPPv2"; Crt::String m_sdkVersion; diff --git a/integration-testing/mosquitto/config/mosquitto.conf b/integration-testing/mosquitto/config/mosquitto.conf new file mode 100644 index 000000000..027e52823 --- /dev/null +++ b/integration-testing/mosquitto/config/mosquitto.conf @@ -0,0 +1,50 @@ +# ====================================================== +# Per-listener settings +# ====================================================== +per_listener_settings true + +# ====================================================== +# 1️⃣ Plain MQTT (no TLS) +# ====================================================== +listener 1883 0.0.0.0 +protocol mqtt +allow_anonymous true +persistence true + +# ====================================================== +# 2️⃣ MQTT over TLS +# ====================================================== +listener 8883 0.0.0.0 +protocol mqtt +allow_anonymous true +require_certificate false +cafile /etc/mosquitto/certs/ca_certificate.pem +certfile /etc/mosquitto/certs/server.crt +keyfile /etc/mosquitto/certs/server.key + +# ====================================================== +# 3️⃣ MQTT over WebSockets (no TLS) +# ====================================================== +listener 8080 0.0.0.0 +protocol websockets +allow_anonymous true + +# ====================================================== +# 4️⃣ MQTT over WebSockets + TLS +# ====================================================== +listener 8081 0.0.0.0 +protocol websockets +allow_anonymous true +require_certificate false +cafile /etc/mosquitto/certs/ca_certificate.pem +certfile /etc/mosquitto/certs/server.crt +keyfile /etc/mosquitto/certs/server.key + +# ====================================================== +# Logging and Persistence +# ====================================================== +persistence true +log_type error +log_type warning +log_type notice +log_type information diff --git a/integration-testing/mosquitto/scripts/create_certificates.bash b/integration-testing/mosquitto/scripts/create_certificates.bash new file mode 100755 index 000000000..04835028e --- /dev/null +++ b/integration-testing/mosquitto/scripts/create_certificates.bash @@ -0,0 +1,30 @@ +#!/bin/bash + +IP="localhost" +SUBJECT_CA="/C=DE/ST=Hamburg/L=Test/O=ApexCA/OU=CA/CN=localhost" +SUBJECT_SERVER="/C=DE/ST=Munich/L=Test/O=ApexServer/OU=Server/CN=localhost" +SUBJECT_CLIENT="/C=DE/ST=Berlin/L=Test/O=ApexClient/OU=Client/CN=device" + +function generate_CA () { + echo "$SUBJECT_CA" + openssl req -x509 -nodes -sha256 -newkey rsa:2048 -subj "$SUBJECT_CA" -days 3650 -keyout ca.key -out ca.crt + cat ca.crt > ca_certificate.pem +} + +function generate_server () { + echo "$SUBJECT_SERVER" + openssl req -nodes -sha256 -new -subj "$SUBJECT_SERVER" -keyout server.key -out server.csr + openssl x509 -req -sha256 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 + cat server.crt > broker_certificate.pem +} + +function generate_client () { + echo "$SUBJECT_CLIENT" + openssl req -new -nodes -sha256 -subj "$SUBJECT_CLIENT" -out client.csr -keyout client.key + openssl x509 -req -sha256 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 3650 + cat client.crt > client_certificate.pem +} + +generate_CA +generate_server +generate_client diff --git a/integration-testing/mosquitto/scripts/install_mosquitto_and_copy_certs.bash b/integration-testing/mosquitto/scripts/install_mosquitto_and_copy_certs.bash new file mode 100644 index 000000000..cba58865d --- /dev/null +++ b/integration-testing/mosquitto/scripts/install_mosquitto_and_copy_certs.bash @@ -0,0 +1,17 @@ +#!/bin/bash + +# install mosquitto +sudo apt update +sudo apt install -y mosquitto + +#Copy generated broker certificates +sudo cp broker_* /etc/mosquitto/certs/ +sudo cp ca_certificate.pem /etc/mosquitto/ca_certificates/ +sudo cp mosquitto.conf /etc/mosquitto/conf.d/ + +#Copy generated client certifications +cp client_* /tmp/ +cp ca_certificate.pem /tmp/ + +#Run mosquitto +mosquitto -c /etc/mosquitto/conf.d/mosquitto.conf \ No newline at end of file diff --git a/integration-testing/mqtt5_client_test.bash b/integration-testing/mqtt5_client_test.bash new file mode 100755 index 000000000..91a7d1587 --- /dev/null +++ b/integration-testing/mqtt5_client_test.bash @@ -0,0 +1,166 @@ +#!/bin/bash + +# Set to 1 to use proxy authentication, 0 to disable +USE_PROXY_AUTH=1 + +# Local broker +LOCAL_BROKER_HOST=localhost +LOCAL_CA_FILE=/etc/mosquitto/ca_certificates/ca_certificate.pem + +# Remote broker +REMOTE_BROKER_HOST=test.mosquitto.org +# relative path from build directory +REMOTE_CA_FILE=../integration-testing/mosquitto/certs/mosquitto-org-ca.crt +REMOTE_WS_CA_FILE=../integration-testing/mosquitto/certs/mosquitto-org-wss-ca.crt + +PROXY_HOST=localhost +PROXY_PORT=1080 +PROXY_URI_NOAUTH="socks5h://${PROXY_HOST}:${PROXY_PORT}" +PROXY_URI_AUTH="socks5h://testuser:testpass@${PROXY_HOST}:${PROXY_PORT}" +EXECUTABLE=./bin/mqtt5_socks5_app/mqtt5_socks5_app + +declare -a TEST_NAMES +declare -a TEST_RESULTS +declare -a TEST_CODES + +run_case() { + echo "" + echo "" + local test_title="$1" + echo "===== $test_title =====" + shift + echo "cmd:" + echo "$@" + "$@" + local status=$? + TEST_NAMES+=("$test_title") + TEST_RESULTS+=("$status") + TEST_CODES+=("$status") +} + +print_summary() { + GREEN='\033[0;32m' + RED='\033[0;31m' + NC='\033[0m' # No Color + echo "====================" + echo "Test Summary:" + pass_count=0 + fail_count=0 + for i in "${!TEST_NAMES[@]}"; do + name="${TEST_NAMES[$i]}" + result="${TEST_RESULTS[$i]}" + if [ "$result" -eq 0 ]; then + echo -e "${GREEN}[PASS]${NC} $name" + ((pass_count++)) + else + echo -e "${RED}[FAIL]${NC} $name (exit code ${TEST_CODES[$i]})" + ((fail_count++)) + fi + done + echo "--------------------" + echo "Total: $((pass_count+fail_count)), Passed: $pass_count, Failed: $fail_count" + echo "====================" +} + +# Test case functions (parameterized) +test_direct_mqtt() { + run_case "Direct MQTT (no proxy, no TLS) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 1883 +} + +test_direct_mqtts() { + run_case "Direct MQTTS (no proxy, TLS) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8883 --ca-file "$3" +} + + +test_proxy_mqtt() { + if [ "$USE_PROXY_AUTH" -eq 1 ]; then + run_case "Proxy MQTT (SOCKS5, no TLS, auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 1883 \ + --proxy "$PROXY_URI_AUTH" + else + run_case "Proxy MQTT (SOCKS5, no TLS, no-auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 1883 \ + --proxy "$PROXY_URI_NOAUTH" + fi +} + +test_proxy_mqtts() { + if [ "$USE_PROXY_AUTH" -eq 1 ]; then + run_case "Proxy MQTTS (SOCKS5, TLS, auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8883 \ + --proxy "$PROXY_URI_AUTH" \ + --ca-file "$3" + else + run_case "Proxy MQTTS (SOCKS5, TLS, no-auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8883 \ + --proxy "$PROXY_URI_NOAUTH" \ + --ca-file "$3" + fi +} + +# WebSocket variants +test_direct_mqtt_ws() { + run_case "Direct MQTT over WebSocket (no proxy, no TLS) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8080 --websocket +} + +test_direct_mqtts_ws() { + run_case "Direct MQTTS over WebSocket (no proxy, TLS) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8081 --websocket --ca-file "$3" +} + + +test_proxy_mqtt_ws() { + if [ "$USE_PROXY_AUTH" -eq 1 ]; then + run_case "Proxy MQTT over WebSocket (SOCKS5, no TLS, auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8080 --websocket \ + --proxy "$PROXY_URI_AUTH" + else + run_case "Proxy MQTT over WebSocket (SOCKS5, no TLS, no-auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8080 --websocket \ + --proxy "$PROXY_URI_NOAUTH" + fi +} + +test_proxy_mqtts_ws() { + if [ "$USE_PROXY_AUTH" -eq 1 ]; then + run_case "Proxy MQTTS over WebSocket (SOCKS5, TLS, auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8081 --websocket \ + --proxy "$PROXY_URI_AUTH" \ + --ca-file "$3" + else + run_case "Proxy MQTTS over WebSocket (SOCKS5, TLS, no-auth) [$1]" \ + $EXECUTABLE --broker-host "$2" --broker-port 8081 --websocket \ + --proxy "$PROXY_URI_NOAUTH" \ + --ca-file "$3" + fi +} + + +# Call all test cases for both local and remote brokers (each test on its own line) + +# Direct broker tests (no proxy) +test_direct_mqtt "LOCAL" "$LOCAL_BROKER_HOST" +test_direct_mqtts "LOCAL" "$LOCAL_BROKER_HOST" "$LOCAL_CA_FILE" +test_direct_mqtt_ws "LOCAL" "$LOCAL_BROKER_HOST" +test_direct_mqtts_ws "LOCAL" "$LOCAL_BROKER_HOST" "$LOCAL_CA_FILE" + +test_direct_mqtt "REMOTE" "$REMOTE_BROKER_HOST" +#test_direct_mqtts "REMOTE" "$REMOTE_BROKER_HOST" "$REMOTE_CA_FILE" +test_direct_mqtt_ws "REMOTE" "$REMOTE_BROKER_HOST" +test_direct_mqtts_ws "REMOTE" "$REMOTE_BROKER_HOST" "$REMOTE_WS_CA_FILE" + +# Proxy broker tests +test_proxy_mqtt "LOCAL" "$LOCAL_BROKER_HOST" +test_proxy_mqtts "LOCAL" "$LOCAL_BROKER_HOST" "$LOCAL_CA_FILE" +test_proxy_mqtt_ws "LOCAL" "$LOCAL_BROKER_HOST" +test_proxy_mqtts_ws "LOCAL" "$LOCAL_BROKER_HOST" "$LOCAL_CA_FILE" + +test_proxy_mqtt "REMOTE" "$REMOTE_BROKER_HOST" +#test_proxy_mqtts "REMOTE" "$REMOTE_BROKER_HOST" "$REMOTE_CA_FILE" +test_proxy_mqtt_ws "REMOTE" "$REMOTE_BROKER_HOST" +test_proxy_mqtts_ws "REMOTE" "$REMOTE_BROKER_HOST" "$REMOTE_WS_CA_FILE" + +print_summary diff --git a/integration-testing/mqtt5_client_test.md b/integration-testing/mqtt5_client_test.md new file mode 100644 index 000000000..6b0ee48e1 --- /dev/null +++ b/integration-testing/mqtt5_client_test.md @@ -0,0 +1,118 @@ +# MQTT5 Client Integration Test Documentation + +This document is intended to guide you through testing the changes introduced in this pull request. It provides setup and execution instructions for the integration tests. You may remove this document after the PR has been reviewed and merged. + +## What the Test Does + +The MQTT5 SOCKS5 integration tests verify end-to-end connectivity against a local Mosquitto broker using the sample client in `bin/mqtt5_socks5_app/mqtt5_socks5_app`. The integration test is run using the Python harness (`mqtt5_client_test.py`). + +The Python harness exercises: + +- Direct and proxied connections +- MQTT and MQTT over WebSocket +- Encrypted (TLS) and unencrypted connections +- Authentication via SOCKS5 proxy + +Each test case runs the client with different combinations of protocol (MQTT/WS), encryption (TLS/no TLS), and proxy settings, ensuring all major local connection scenarios are validated. + +## Test Setup + +- The test script runs a series of client invocations against a Mosquitto broker and a SOCKS5 proxy. +- Certificate files for TLS are included in the integration test directory and mounted into the Mosquitto container. +- The script expects the following environment: + - Mosquitto broker accessible at `localhost` (or your chosen host) + - SOCKS5 proxy accessible at `localhost` (or your chosen host) + - Proxy credentials: username `testuser`, password `testpass` + +## How to Setup Mosquitto + +Create required directories for Mosquitto: + +```bash +mkdir -p ~/mosquitto/config +mkdir -p ~/mosquitto/data +mkdir -p ~/mosquitto/log +mkdir -p ~/mosquitto/certs +``` + +Generate certificates for Mosquitto using the provided script: + +```bash +integration-testing/mosquitto/scripts/create_certificates.bash +``` + +Copy generated broker certificates + +```bash +sudo cp broker_* /etc/mosquitto/certs/ +sudo cp ca_certificate.pem /etc/mosquitto/ca_certificates/ +sudo cp mosquitto.conf /etc/mosquitto/conf.d/ +``` + +Start the Mosquitto broker: + +```bash +mosquitto -c /etc/mosquitto/conf.d/mosquitto.conf +``` + +- Make sure your config, data, log, and certs directories exist and are populated as needed. +- The config file should enable listeners for all required ports and protocols (MQTT, MQTT+WS, TLS). +- The cert files for TLS will be generated and installed by the scripts above. + +## How to Setup SOCKS5 Proxy + +Start the SOCKS5 proxy: + +```bash +docker run -p 1080:1080 \ + -e PROXY_USER=testuser -e PROXY_PASSWORD=testpass \ + serjs/go-socks5-proxy +``` + +- This will start a SOCKS5 proxy on localhost port 1080 with authentication enabled. +- The proxy will be accessible at `localhost:1080`. + +## How to Build and Run the Test + +### Build the Project + +Make sure you have all dependencies installed (CMake, a C/C++ compiler, etc.). From the project root: + +```bash +mkdir -p build +cd build +cmake .. +make -j$(nproc) +``` + +This will build all binaries required for the integration tests. + +### Run the Integration Test + +After building, you can run the Python harness from the build directory: + +#### Python + +```bash +python3 ../integration-testing/mqtt5_client_test.py ./bin/mqtt5_socks5_app/mqtt5_socks5_app +``` + +- Optional environment overrides: + - `MQTT5_LOCAL_BROKER_HOST` + - `MQTT5_LOCAL_CA_FILE` + - `MQTT5_PROXY_HOST`, `MQTT5_PROXY_PORT`, `MQTT5_PROXY_USER`, `MQTT5_PROXY_PASSWORD`, `MQTT5_PROXY_USE_AUTH` +- Tests requiring a CA certificate automatically skip if the file is not found (useful when the local Mosquitto TLS setup is absent). + +## Notes + +- All integration test scripts and client invocations should use `localhost` for both Mosquitto and SOCKS5 proxy hosts. +- Certificate files for TLS are generated and installed locally. +- You can modify the Python harness to test other brokers, proxies, or authentication setups as needed. + +## Example Test Cases Covered + +- Direct MQTT (1883), no TLS +- Direct MQTT (8883), TLS +- Direct MQTT over WebSocket (8080/8081), with/without TLS +- Proxy MQTT (1883/8883), with/without TLS, with authentication +- Proxy MQTT over WebSocket (8080/8081), with/without TLS, with authentication diff --git a/integration-testing/mqtt5_client_test.py b/integration-testing/mqtt5_client_test.py new file mode 100644 index 000000000..ade16f957 --- /dev/null +++ b/integration-testing/mqtt5_client_test.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0. +""" + +import os +import subprocess +import sys +import unittest +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional +from urllib.parse import quote + +TIMEOUT_SECONDS = 120 + +COMMAND_PREFIX = sys.argv[1:] +if not COMMAND_PREFIX: + print("You must pass the mqtt5_socks5_app command prefix, e.g. python mqtt5_client_test.py ./bin/mqtt5_socks5_app/mqtt5_socks5_app") # noqa: E501 + sys.exit(-1) + +PROGRAM = COMMAND_PREFIX[0] + +if "bin" in PROGRAM and not Path(PROGRAM).exists(): + print(f"{PROGRAM} not found, skipping MQTT5 SOCKS5 integration tests.") + sys.exit(0) + +# Ensure unittest does not attempt to parse our custom arguments. +sys.argv = sys.argv[:1] + +SCRIPT_DIR = Path(__file__).resolve().parent + +LOCAL_BROKER_HOST = os.environ.get("MQTT5_LOCAL_BROKER_HOST", "localhost") +LOCAL_CA_FILE = os.environ.get("MQTT5_LOCAL_CA_FILE", "/etc/mosquitto/ca_certificates/ca_certificate.pem") + +PROXY_HOST = os.environ.get("MQTT5_PROXY_HOST", "localhost") +PROXY_PORT = os.environ.get("MQTT5_PROXY_PORT", "1080") +PROXY_USER = os.environ.get("MQTT5_PROXY_USER", "testuser") +PROXY_PASSWORD = os.environ.get("MQTT5_PROXY_PASSWORD", "testpass") +USE_PROXY_AUTH = os.environ.get("MQTT5_PROXY_USE_AUTH", "1").lower() not in ("0", "false", "no") + + +# socks5h (host resolution by proxy) +PROXY_URI_NOAUTH = f"socks5h://{PROXY_HOST}:{PROXY_PORT}" +PROXY_URI_AUTH = f"socks5h://{quote(PROXY_USER)}:{quote(PROXY_PASSWORD)}@{PROXY_HOST}:{PROXY_PORT}" +PROXY_URI = PROXY_URI_AUTH if USE_PROXY_AUTH else PROXY_URI_NOAUTH + +# socks5 (client host resolution) +PROXY_SOCKS5_NOAUTH = f"socks5://{PROXY_HOST}:{PROXY_PORT}" +PROXY_SOCKS5_AUTH = f"socks5://{quote(PROXY_USER)}:{quote(PROXY_PASSWORD)}@{PROXY_HOST}:{PROXY_PORT}" +PROXY_SOCKS5_URI = PROXY_SOCKS5_AUTH if USE_PROXY_AUTH else PROXY_SOCKS5_NOAUTH + + +def run_command(args: List[str], label: Optional[str] = None) -> None: + """Run the provided command and raise with helpful diagnostics on failure.""" + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + timed_out = False + try: + output = process.communicate(timeout=TIMEOUT_SECONDS)[0] + except subprocess.TimeoutExpired: + timed_out = True + process.kill() + output = process.communicate()[0] + if process.returncode != 0 or timed_out: + decoded_output = output.decode(errors="replace") + args_str = subprocess.list2cmdline(args) + heading = f"[{label}] " if label else "" + print(f"{heading}{args_str}") + print(decoded_output) + if timed_out: + raise RuntimeError( + f"{heading}Timeout after {TIMEOUT_SECONDS}s running: {args_str}\n{decoded_output}" + ) + raise RuntimeError( + f"{heading}Return code {process.returncode} from: {args_str}\n{decoded_output}" + ) + + +@dataclass(frozen=True) +class TestCaseConfig: + name: str + broker_host: str + broker_port: int + websocket: bool = False + tls_ca_file: Optional[str] = None + proxy_uri: Optional[str] = None + + +def _build_cases() -> List[TestCaseConfig]: + cases: List[TestCaseConfig] = [] + + proxy = PROXY_URI + + # Direct connections (no proxy) + cases.extend( + [ + TestCaseConfig("direct_mqtt_local", LOCAL_BROKER_HOST, 1883), + TestCaseConfig("direct_mqtts_local", LOCAL_BROKER_HOST, 8883, tls_ca_file=LOCAL_CA_FILE), + TestCaseConfig("direct_mqtt_ws_local", LOCAL_BROKER_HOST, 8080, websocket=True), + TestCaseConfig("direct_mqtts_ws_local", LOCAL_BROKER_HOST, 8081, websocket=True, tls_ca_file=LOCAL_CA_FILE), + ] + ) + + # Proxy connections + cases.extend( + [ + TestCaseConfig("proxy_mqtt_local", LOCAL_BROKER_HOST, 1883, proxy_uri=proxy), + TestCaseConfig("proxy_mqtts_local", LOCAL_BROKER_HOST, 8883, tls_ca_file=LOCAL_CA_FILE, proxy_uri=proxy), + TestCaseConfig("proxy_mqtt_ws_local", LOCAL_BROKER_HOST, 8080, websocket=True, proxy_uri=proxy), + TestCaseConfig( + "proxy_mqtts_ws_local", LOCAL_BROKER_HOST, 8081, websocket=True, tls_ca_file=LOCAL_CA_FILE, proxy_uri=proxy + ), + ] + ) + + # Additional test cases for client host resolution: socks5 (not socks5h) + cases.extend([ + TestCaseConfig("proxy_mqtt_local_socks5", LOCAL_BROKER_HOST, 1883, proxy_uri=PROXY_SOCKS5_URI), + TestCaseConfig("proxy_mqtts_local_socks5", LOCAL_BROKER_HOST, 8883, tls_ca_file=LOCAL_CA_FILE, proxy_uri=PROXY_SOCKS5_URI), + TestCaseConfig("proxy_mqtt_ws_local_socks5", LOCAL_BROKER_HOST, 8080, websocket=True, proxy_uri=PROXY_SOCKS5_URI), + TestCaseConfig( + "proxy_mqtts_ws_local_socks5", LOCAL_BROKER_HOST, 8081, websocket=True, tls_ca_file=LOCAL_CA_FILE, proxy_uri=PROXY_SOCKS5_URI + ), + ]) + return cases + + +class Mqtt5Socks5IntegrationTests(unittest.TestCase): + @staticmethod + def _build_command(case: TestCaseConfig) -> List[str]: + args: List[str] = [ + *COMMAND_PREFIX, + "--broker-host", + case.broker_host, + "--broker-port", + str(case.broker_port), + ] + if case.websocket: + args.append("--websocket") + if case.tls_ca_file: + args.extend(["--ca-file", case.tls_ca_file]) + if case.proxy_uri: + args.extend(["--proxy", case.proxy_uri]) + return args + + def _run_case(self, case: TestCaseConfig) -> None: + if case.tls_ca_file and not Path(case.tls_ca_file).exists(): + self.skipTest(f"CA file '{case.tls_ca_file}' not found") + if case.proxy_uri and not PROXY_URI: + self.skipTest("Proxy URI not configured") + try: + run_command(self._build_command(case), label=case.name) + except RuntimeError as exc: + self.fail(str(exc)) + + +# Dynamically add one unittest.TestCase method per configuration for easy filtering. +for _case in _build_cases(): + def _make_test(case: TestCaseConfig): + def _test(self: Mqtt5Socks5IntegrationTests) -> None: + self._run_case(case) + + _test.__name__ = f"test_{case.name}" + _test.__doc__ = ( + f"MQTT5 socks5 scenario '{case.name}': " + f"{case.broker_host}:{case.broker_port}, websocket={case.websocket}, proxy={'yes' if case.proxy_uri else 'no'}, " + f"tls={'yes' if case.tls_ca_file else 'no'}" + ) + setattr(Mqtt5Socks5IntegrationTests, _test.__name__, _test) + + _make_test(_case) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/source/auth/Credentials.cpp b/source/auth/Credentials.cpp index e5ddd8978..482ff0d9b 100644 --- a/source/auth/Credentials.cpp +++ b/source/auth/Credentials.cpp @@ -337,6 +337,13 @@ namespace Aws raw_config.proxy_options = &proxy_options; } + const struct aws_socks5_proxy_options *socks5_proxy_options = nullptr; + if (config.Socks5ProxyOptions.has_value()) + { + socks5_proxy_options = config.Socks5ProxyOptions->GetUnderlyingHandle(); + raw_config.socks5_proxy_options = socks5_proxy_options; + } + return s_CreateWrappedProvider(aws_credentials_provider_new_x509(allocator, &raw_config), allocator); } diff --git a/source/http/HttpConnection.cpp b/source/http/HttpConnection.cpp index cbdace01a..7c0e937c6 100644 --- a/source/http/HttpConnection.cpp +++ b/source/http/HttpConnection.cpp @@ -160,6 +160,11 @@ namespace Aws options.on_shutdown = HttpClientConnection::s_onClientConnectionShutdown; options.manual_window_management = connectionOptions.ManualWindowManagement; + if (connectionOptions.Socks5ProxyOptions) + { + options.socks5_proxy_options = connectionOptions.Socks5ProxyOptions->GetUnderlyingHandle(); + } + aws_http_proxy_options proxyOptions; AWS_ZERO_STRUCT(proxyOptions); if (connectionOptions.ProxyOptions) @@ -405,5 +410,5 @@ namespace Aws { } } // namespace Http - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/source/http/HttpConnectionManager.cpp b/source/http/HttpConnectionManager.cpp index 4ec879ec8..e92057fad 100644 --- a/source/http/HttpConnectionManager.cpp +++ b/source/http/HttpConnectionManager.cpp @@ -112,9 +112,15 @@ namespace Aws m_shutdownPromise.set_value(); } + if (connectionOptions.Socks5ProxyOptions) + { + managerOptions.socks5_proxy_options = connectionOptions.Socks5ProxyOptions->GetUnderlyingHandle(); + } + aws_http_proxy_options proxyOptions; AWS_ZERO_STRUCT(proxyOptions); - if (connectionOptions.ProxyOptions) + // Socks5ProxyOptions take precedence over ProxyOptions + if (!connectionOptions.Socks5ProxyOptions && connectionOptions.ProxyOptions) { /* This is verified by HttpClientConnectionManager::NewClientConnectionManager */ AWS_FATAL_ASSERT( @@ -232,5 +238,5 @@ namespace Aws } } // namespace Http - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/source/io/Socks5ProxyOptions.cpp b/source/io/Socks5ProxyOptions.cpp new file mode 100644 index 000000000..173cacb80 --- /dev/null +++ b/source/io/Socks5ProxyOptions.cpp @@ -0,0 +1,471 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace Aws +{ + namespace Crt + { + namespace Io + { + + Socks5ProxyOptions::Socks5ProxyOptions() noexcept + : m_allocator(aws_default_allocator()), m_lastError(AWS_ERROR_SUCCESS) + { + if (aws_socks5_proxy_options_init_default(&m_options)) + { + m_lastError = aws_last_error(); + } + } + + //TODO: remove deprecated constructor + Socks5ProxyOptions::Socks5ProxyOptions( + const String &hostName, + uint32_t port, + AwsSocks5AuthMethod authMethod, + const String &username, + const String &password, + uint32_t connectionTimeoutMs, + struct aws_allocator *allocator, + AwsSocks5HostResolutionMode resolutionMode) + : Socks5ProxyOptions() + { + m_allocator = allocator ? allocator : aws_default_allocator(); + + if (!SetProxyEndpoint(hostName, port)) + { + return; + } + + SetConnectionTimeoutMs(connectionTimeoutMs); + SetHostResolutionMode(resolutionMode); + + if (authMethod == AwsSocks5AuthMethod::UsernamePassword) + { + if (!SetAuthCredentials(username, password)) + { + return; + } + } + + m_lastError = AWS_ERROR_SUCCESS; + } + + Socks5ProxyOptions::Socks5ProxyOptions( + const String &hostName, + uint32_t port, + const Socks5ProxyAuthConfig &authConfig, + uint32_t connectionTimeoutMs, + AwsSocks5HostResolutionMode resolutionMode, + struct aws_allocator *allocator) + : Socks5ProxyOptions() + { + m_allocator = allocator ? allocator : aws_default_allocator(); + + if (!SetProxyEndpoint(hostName, port)) + { + return; + } + + SetConnectionTimeoutMs(connectionTimeoutMs); + SetHostResolutionMode(resolutionMode); + + if (!SetAuth(authConfig)) + { + return; + } + + m_lastError = AWS_ERROR_SUCCESS; + } + + Socks5ProxyOptions::~Socks5ProxyOptions() + { + aws_socks5_proxy_options_clean_up(&m_options); + } + + Socks5ProxyOptions::operator bool() const noexcept + { + return m_options.host != nullptr; + } + + int Socks5ProxyOptions::LastError() const noexcept + { + return m_lastError; + } + + bool Socks5ProxyOptions::SetProxyEndpoint(const String &hostName, uint32_t port) + { + if (hostName.empty() || port > UINT16_MAX) + { + m_lastError = AWS_ERROR_INVALID_ARGUMENT; + return false; + } + + struct aws_allocator *allocator = m_allocator ? m_allocator : aws_default_allocator(); + m_allocator = allocator; + + aws_socks5_proxy_options newOptions; + AWS_ZERO_STRUCT(newOptions); + + aws_byte_cursor hostCursor = + aws_byte_cursor_from_array(reinterpret_cast(hostName.data()), hostName.length()); + + if (aws_socks5_proxy_options_init(&newOptions, allocator, hostCursor, static_cast(port))) + { + m_lastError = aws_last_error(); + aws_socks5_proxy_options_clean_up(&newOptions); + return false; + } + + uint32_t previousTimeout = m_options.connection_timeout_ms; + AwsSocks5HostResolutionMode previousMode = GetHostResolutionMode(); + newOptions.connection_timeout_ms = previousTimeout; + aws_socks5_proxy_options_set_host_resolution_mode( + &newOptions, static_cast(previousMode)); + + if (!ApplyAuthConfig(newOptions, m_authConfig)) + { + aws_socks5_proxy_options_clean_up(&newOptions); + return false; + } + + aws_socks5_proxy_options_clean_up(&m_options); + m_options = newOptions; + AWS_ZERO_STRUCT(newOptions); + + m_lastError = AWS_ERROR_SUCCESS; + return true; + } + + bool Socks5ProxyOptions::SetAuth(const Socks5ProxyAuthConfig &authConfig) + { + if (authConfig.Method == AwsSocks5AuthMethod::None) + { + if (authConfig.Username.has_value() || authConfig.Password.has_value()) + { + m_lastError = AWS_ERROR_INVALID_ARGUMENT; + return false; + } + } + else if (authConfig.Method == AwsSocks5AuthMethod::UsernamePassword) + { + if (!authConfig.Username.has_value() || authConfig.Username->empty() || + !authConfig.Password.has_value() || authConfig.Password->empty()) + { + m_lastError = AWS_ERROR_INVALID_ARGUMENT; + return false; + } + } + else + { + m_lastError = AWS_ERROR_INVALID_ARGUMENT; + return false; + } + + if (!ApplyAuthConfig(m_options, authConfig)) + { + return false; + } + + m_authConfig = authConfig; + m_lastError = AWS_ERROR_SUCCESS; + return true; + } + + bool Socks5ProxyOptions::SetAuthCredentials(const String &username, const String &password) + { + return SetAuth(Socks5ProxyAuthConfig::CreateUsernamePassword(username, password)); + } + + void Socks5ProxyOptions::ClearAuthCredentials() + { + Socks5ProxyAuthConfig noneConfig = Socks5ProxyAuthConfig::CreateNone(); + (void)SetAuth(noneConfig); + } + + void Socks5ProxyOptions::SetConnectionTimeoutMs(uint32_t timeoutMs) + { + m_options.connection_timeout_ms = timeoutMs; + } + + Socks5ProxyOptions::Socks5ProxyOptions(const Socks5ProxyOptions &other) : Socks5ProxyOptions() + { + m_allocator = other.m_allocator ? other.m_allocator : aws_default_allocator(); + m_lastError = AWS_ERROR_SUCCESS; + aws_socks5_proxy_options_clean_up(&m_options); + if (aws_socks5_proxy_options_copy(&m_options, &other.m_options) != AWS_OP_SUCCESS) + { + m_lastError = aws_last_error(); + aws_socks5_proxy_options_init_default(&m_options); + m_authConfig = Socks5ProxyAuthConfig::CreateNone(); + } + else + { + m_authConfig = other.m_authConfig; + } + } + + Socks5ProxyOptions::Socks5ProxyOptions(Socks5ProxyOptions &&other) noexcept + : m_options(other.m_options), m_allocator(other.m_allocator), m_lastError(other.m_lastError), + m_authConfig(std::move(other.m_authConfig)) + { + AWS_ZERO_STRUCT(other.m_options); + other.m_allocator = aws_default_allocator(); + other.m_lastError = AWS_ERROR_SUCCESS; + other.m_authConfig = Socks5ProxyAuthConfig::CreateNone(); + } + + Socks5ProxyOptions &Socks5ProxyOptions::operator=(const Socks5ProxyOptions &other) + { + if (this != &other) + { + aws_socks5_proxy_options_clean_up(&m_options); + m_allocator = other.m_allocator ? other.m_allocator : aws_default_allocator(); + if (aws_socks5_proxy_options_copy(&m_options, &other.m_options) != AWS_OP_SUCCESS) + { + m_lastError = aws_last_error(); + aws_socks5_proxy_options_init_default(&m_options); + m_authConfig = Socks5ProxyAuthConfig::CreateNone(); + } + else + { + m_lastError = AWS_ERROR_SUCCESS; + m_authConfig = other.m_authConfig; + } + } + return *this; + } + + Socks5ProxyOptions &Socks5ProxyOptions::operator=(Socks5ProxyOptions &&other) noexcept + { + if (this != &other) + { + aws_socks5_proxy_options_clean_up(&m_options); + m_options = other.m_options; + m_allocator = other.m_allocator; + m_lastError = other.m_lastError; + m_authConfig = std::move(other.m_authConfig); + AWS_ZERO_STRUCT(other.m_options); + other.m_allocator = aws_default_allocator(); + other.m_lastError = AWS_ERROR_SUCCESS; + other.m_authConfig = Socks5ProxyAuthConfig::CreateNone(); + } + return *this; + } + + Optional Socks5ProxyOptions::GetHost() const + { + if (m_options.host && m_options.host->len > 0) + { + return String(reinterpret_cast(m_options.host->bytes), m_options.host->len); + } + return Optional(); + } + + uint16_t Socks5ProxyOptions::GetPort() const + { + return m_options.port; + } + + Optional Socks5ProxyOptions::GetUsername() const + { + if (m_options.username && m_options.username->len > 0) + { + return String(reinterpret_cast(m_options.username->bytes), m_options.username->len); + } + return Optional(); + } + + Optional Socks5ProxyOptions::GetPassword() const + { + if (m_options.password && m_options.password->len > 0) + { + return String(reinterpret_cast(m_options.password->bytes), m_options.password->len); + } + return Optional(); + } + + uint32_t Socks5ProxyOptions::GetConnectionTimeoutMs() const + { + return m_options.connection_timeout_ms; + } + + AwsSocks5AuthMethod Socks5ProxyOptions::GetAuthMethod() const + { + return m_authConfig.Method; + } + + AwsSocks5HostResolutionMode Socks5ProxyOptions::GetResolutionMode() const + { + return GetHostResolutionMode(); + } + + Optional Socks5ProxyOptions::CreateFromUri( + const Uri &uri, + uint32_t connectionTimeoutMs, + struct aws_allocator *allocator) + { + ByteCursor schemeCursor = uri.GetScheme(); + if (schemeCursor.len == 0) + { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return Optional(); + } + + String scheme(reinterpret_cast(schemeCursor.ptr), schemeCursor.len); + std::transform( + scheme.begin(), + scheme.end(), + scheme.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + if (scheme != "socks5" && scheme != "socks5h") + { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return Optional(); + } + + AwsSocks5HostResolutionMode resolutionMode = + (scheme == "socks5h") ? AwsSocks5HostResolutionMode::Proxy : AwsSocks5HostResolutionMode::Client; + + ByteCursor hostCursor = uri.GetHostName(); + if (hostCursor.len == 0) + { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return Optional(); + } + + String host(reinterpret_cast(hostCursor.ptr), hostCursor.len); + + uint32_t port = uri.GetPort(); + if (port == 0) + { + port = Socks5ProxyOptions::DefaultProxyPort; + } + if (port > UINT16_MAX) + { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return Optional(); + } + + String username; + String password; + + ByteCursor authorityCursor = uri.GetAuthority(); + if (authorityCursor.len > 0) + { + String authority(reinterpret_cast(authorityCursor.ptr), authorityCursor.len); + auto atPos = authority.find('@'); + if (atPos != String::npos) + { + String userinfo = authority.substr(0, atPos); + auto colonPos = userinfo.find(':'); + if (colonPos == String::npos) + { + username = userinfo; + } + else + { + username = userinfo.substr(0, colonPos); + password = userinfo.substr(colonPos + 1); + } + } + } + + Socks5ProxyAuthConfig authConfig = Socks5ProxyAuthConfig::CreateNone(); + if (!username.empty() && !password.empty()) + { + authConfig = Socks5ProxyAuthConfig::CreateUsernamePassword(username, password); + } + + Socks5ProxyOptions options( + host, + static_cast(port), + authConfig, + connectionTimeoutMs, + resolutionMode, + allocator); + + if (!options) + { + return Optional(); + } + + if (options.LastError() != AWS_ERROR_SUCCESS) + { + aws_raise_error(options.LastError()); + return Optional(); + } + + return Optional(std::move(options)); + } + + void Socks5ProxyOptions::SetHostResolutionMode(AwsSocks5HostResolutionMode mode) + { + aws_socks5_proxy_options_set_host_resolution_mode( + &m_options, static_cast(mode)); + } + + AwsSocks5HostResolutionMode Socks5ProxyOptions::GetHostResolutionMode() const + { + return static_cast( + aws_socks5_proxy_options_get_host_resolution_mode(&m_options)); + } + + bool Socks5ProxyOptions::ApplyAuthConfig( + aws_socks5_proxy_options &options, + const Socks5ProxyAuthConfig &authConfig) + { + struct aws_allocator *allocator = m_allocator ? m_allocator : aws_default_allocator(); + m_allocator = allocator; + + switch (authConfig.Method) + { + case AwsSocks5AuthMethod::None: + { + aws_byte_cursor emptyCursor; + AWS_ZERO_STRUCT(emptyCursor); + if (aws_socks5_proxy_options_set_auth(&options, allocator, emptyCursor, emptyCursor)) + { + m_lastError = aws_last_error(); + return false; + } + return true; + } + case AwsSocks5AuthMethod::UsernamePassword: + { + if (!authConfig.Username.has_value() || authConfig.Username->empty() || + !authConfig.Password.has_value() || authConfig.Password->empty()) + { + m_lastError = AWS_ERROR_INVALID_ARGUMENT; + return false; + } + + aws_byte_cursor usernameCursor = aws_byte_cursor_from_array( + reinterpret_cast(authConfig.Username->data()), authConfig.Username->length()); + aws_byte_cursor passwordCursor = aws_byte_cursor_from_array( + reinterpret_cast(authConfig.Password->data()), authConfig.Password->length()); + + if (aws_socks5_proxy_options_set_auth(&options, allocator, usernameCursor, passwordCursor)) + { + m_lastError = aws_last_error(); + return false; + } + return true; + } + default: + m_lastError = AWS_ERROR_INVALID_ARGUMENT; + return false; + } + } + + } // namespace Io + } // namespace Crt +} // namespace Aws diff --git a/source/iot/Mqtt5Client.cpp b/source/iot/Mqtt5Client.cpp index 18fde4ec4..5ed61717c 100644 --- a/source/iot/Mqtt5Client.cpp +++ b/source/iot/Mqtt5Client.cpp @@ -368,6 +368,13 @@ namespace Aws return *this; } + Mqtt5ClientBuilder &Mqtt5ClientBuilder::WithSocks5ProxyOptions( + const Crt::Io::Socks5ProxyOptions &proxyOptions) noexcept + { + m_socks5ProxyOptions = proxyOptions; + return *this; + } + Mqtt5ClientBuilder &Mqtt5ClientBuilder::WithCustomAuthorizer(const Iot::Mqtt5CustomAuthConfig &config) noexcept { m_customAuthConfig = config; @@ -623,6 +630,11 @@ namespace Aws } } + if (m_socks5ProxyOptions.has_value()) + { + m_options->WithSocks5ProxyOptions(m_socks5ProxyOptions.value()); + } + if (m_proxyOptions.has_value() && !proxyOptionsSet) { m_options->WithHttpProxyOptions(m_proxyOptions.value()); diff --git a/source/iot/MqttClient.cpp b/source/iot/MqttClient.cpp index daed7385a..3f8bc9236 100644 --- a/source/iot/MqttClient.cpp +++ b/source/iot/MqttClient.cpp @@ -43,9 +43,11 @@ namespace Aws const Crt::Io::SocketOptions &socketOptions, Crt::Io::TlsContext &&tlsContext, Crt::Mqtt::OnWebSocketHandshakeIntercept &&interceptor, - const Crt::Optional &proxyOptions) + const Crt::Optional &proxyOptions, + const Crt::Optional &socks5ProxyOptions) : m_endpoint(endpoint), m_port(port), m_context(std::move(tlsContext)), m_socketOptions(socketOptions), - m_webSocketInterceptor(std::move(interceptor)), m_proxyOptions(proxyOptions), m_lastError(0) + m_webSocketInterceptor(std::move(interceptor)), m_proxyOptions(proxyOptions), + m_socks5ProxyOptions(socks5ProxyOptions), m_lastError(0) { } @@ -54,9 +56,10 @@ namespace Aws uint32_t port, const Crt::Io::SocketOptions &socketOptions, Crt::Io::TlsContext &&tlsContext, - const Crt::Optional &proxyOptions) + const Crt::Optional &proxyOptions, + const Crt::Optional &socks5ProxyOptions) : m_endpoint(endpoint), m_port(port), m_context(std::move(tlsContext)), m_socketOptions(socketOptions), - m_proxyOptions(proxyOptions), m_lastError(0) + m_proxyOptions(proxyOptions), m_socks5ProxyOptions(socks5ProxyOptions), m_lastError(0) { } @@ -532,7 +535,7 @@ namespace Aws if (!m_websocketConfig) { auto config = MqttClientConnectionConfig( - m_endpoint, port, m_socketOptions, std::move(tlsContext), m_proxyOptions); + m_endpoint, port, m_socketOptions, std::move(tlsContext), m_proxyOptions, m_socks5ProxyOptions); config.m_username = username; config.m_password = password; return config; @@ -562,7 +565,8 @@ namespace Aws m_socketOptions, std::move(tlsContext), signerTransform, - useWebsocketProxyOptions ? m_websocketConfig->ProxyOptions : m_proxyOptions); + useWebsocketProxyOptions ? m_websocketConfig->ProxyOptions : m_proxyOptions, + m_socks5ProxyOptions); config.m_username = username; config.m_password = password; return config; @@ -626,6 +630,11 @@ namespace Aws newConnection->SetHttpProxyOptions(config.m_proxyOptions.value()); } + if (config.m_socks5ProxyOptions) + { + newConnection->SetSocks5ProxyOptions(config.m_socks5ProxyOptions.value()); + } + return newConnection; } } // namespace Iot diff --git a/source/mqtt/Mqtt5Client.cpp b/source/mqtt/Mqtt5Client.cpp index 4d40eb7f1..8939e62f0 100644 --- a/source/mqtt/Mqtt5Client.cpp +++ b/source/mqtt/Mqtt5Client.cpp @@ -199,6 +199,8 @@ namespace Aws AWS_ZERO_STRUCT(m_packetConnectViewStorage); AWS_ZERO_STRUCT(m_httpProxyOptionsStorage); + AWS_ZERO_STRUCT(m_socks5ProxyOptionsStorage); + AWS_ZERO_STRUCT(m_topicAliasingOptions); } @@ -228,6 +230,11 @@ namespace Aws raw_options.http_proxy_options = &m_httpProxyOptionsStorage; } + if (m_socks5ProxyOptions.has_value()) + { + raw_options.socks5_proxy_options = m_socks5ProxyOptions->GetUnderlyingHandle(); + } + raw_options.connect_options = &m_packetConnectViewStorage; raw_options.session_behavior = m_sessionBehavior; raw_options.extended_validation_and_flow_control_options = m_extendedValidationAndFlowControlOptions; @@ -244,6 +251,12 @@ namespace Aws return true; } + Mqtt5ClientOptions &Mqtt5ClientOptions::WithSocks5ProxyOptions( + const Crt::Io::Socks5ProxyOptions &proxyOptions) noexcept + { + m_socks5ProxyOptions = proxyOptions; + return *this; + } Mqtt5ClientOptions::~Mqtt5ClientOptions() {} @@ -418,5 +431,5 @@ namespace Aws } } // namespace Mqtt5 - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/source/mqtt/MqttConnection.cpp b/source/mqtt/MqttConnection.cpp index 043c3123a..0433de0f4 100644 --- a/source/mqtt/MqttConnection.cpp +++ b/source/mqtt/MqttConnection.cpp @@ -164,6 +164,12 @@ namespace Aws return m_connectionCore->SetHttpProxyOptions(proxyOptions); } + bool MqttConnection::SetSocks5ProxyOptions(const Io::Socks5ProxyOptions &options) noexcept + { + AWS_ASSERT(m_connectionCore != nullptr); + return m_connectionCore->SetSocks5ProxyOptions(options); + } + bool MqttConnection::SetReconnectTimeout(uint64_t min_seconds, uint64_t max_seconds) noexcept { AWS_ASSERT(m_connectionCore != nullptr); @@ -296,5 +302,5 @@ namespace Aws return m_connectionCore->GetOperationStatistics(); } } // namespace Mqtt - } // namespace Crt + } // namespace Crt } // namespace Aws diff --git a/source/mqtt/MqttConnectionCore.cpp b/source/mqtt/MqttConnectionCore.cpp index a360dfd04..1ae3523e1 100644 --- a/source/mqtt/MqttConnectionCore.cpp +++ b/source/mqtt/MqttConnectionCore.cpp @@ -595,6 +595,20 @@ namespace Aws return true; } + bool MqttConnectionCore::SetSocks5ProxyOptions(const Io::Socks5ProxyOptions &proxyOptions) noexcept + { + Io::Socks5ProxyOptions optionsCopy(proxyOptions); + + if (aws_mqtt_client_connection_set_socks5_proxy_options( + m_underlyingConnection, optionsCopy.GetUnderlyingHandle()) != AWS_OP_SUCCESS) + { + return false; + } + + m_socks5ProxyOptions = optionsCopy; + return true; + } + bool MqttConnectionCore::SetReconnectTimeout(uint64_t min_seconds, uint64_t max_seconds) noexcept { return aws_mqtt_client_connection_set_reconnect_timeout( @@ -660,6 +674,15 @@ namespace Aws } } + if (m_socks5ProxyOptions) + { + if (aws_mqtt_client_connection_set_socks5_proxy_options( + m_underlyingConnection, m_socks5ProxyOptions->GetUnderlyingHandle()) != AWS_OP_SUCCESS) + { + return false; + } + } + return aws_mqtt_client_connection_connect(m_underlyingConnection, &options) == AWS_OP_SUCCESS; } @@ -924,6 +947,6 @@ namespace Aws return m_operationStatistics; } } // namespace Mqtt - } // namespace Crt + } // namespace Crt } // namespace Aws /*! \endcond */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 642aa17a4..382f0fc65 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -121,6 +121,14 @@ add_test_case(StreamTestReadEmpty) add_test_case(StreamTestSeekBegin) add_test_case(StreamTestSeekEnd) add_test_case(StreamTestRefcount) +add_test_case(Socks5ProxyOptionsCreateFromUriNoAuth) +add_test_case(Socks5ProxyOptionsCreateFromUriAuth) +add_test_case(Socks5ProxyOptionsCreateFromUriInvalid) +add_test_case(Socks5ProxyOptionsCtorDefaults) +add_test_case(Socks5ProxyOptionsIgnoreCredentialsWhenAuthNone) +add_test_case(Socks5ProxyOptionsCopyAndMove) +add_test_case(Socks5ProxyOptionsSetters) +add_test_case(Socks5ProxyOptionsAuthConfig) add_test_case(TestCredentialsConstruction) add_test_case(TestCredentialsConstructionWithAccountId) add_test_case(TestAnonymousCredentialsConstruction) diff --git a/tests/Socks5ProxyOptionsTest.cpp b/tests/Socks5ProxyOptionsTest.cpp new file mode 100644 index 000000000..d897b4540 --- /dev/null +++ b/tests/Socks5ProxyOptionsTest.cpp @@ -0,0 +1,353 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include +#include +#include + +using namespace Aws::Crt; +using namespace Aws::Crt::Io; + +static int s_TestSocks5ProxyOptionsCreateFromUriNoAuth(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + const char *proxyUri = "socks5://proxy.example.com:1081"; + Uri uri(aws_byte_cursor_from_c_str(proxyUri), allocator); + ASSERT_TRUE(uri); + + auto options = Socks5ProxyOptions::CreateFromUri(uri, 5000 /* timeout ms */, allocator); + ASSERT_TRUE(options.has_value()); + + const aws_socks5_proxy_options *raw = options->GetUnderlyingHandle(); + ASSERT_NOT_NULL(raw); + ASSERT_NOT_NULL(raw->host); + ASSERT_STR_EQUALS("proxy.example.com", aws_string_c_str(raw->host)); + ASSERT_INT_EQUALS(1081, raw->port); + ASSERT_UINT_EQUALS(5000, raw->connection_timeout_ms); + ASSERT_TRUE(*options); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options->LastError()); + ASSERT_INT_EQUALS(static_cast(AwsSocks5AuthMethod::None), static_cast(options->GetAuthMethod())); + ASSERT_TRUE(raw->username == NULL); + ASSERT_TRUE(raw->password == NULL); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Client), static_cast(options->GetHostResolutionMode())); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Client), static_cast(options->GetResolutionMode())); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsCreateFromUriNoAuth, s_TestSocks5ProxyOptionsCreateFromUriNoAuth) + +static int s_TestSocks5ProxyOptionsCreateFromUriAuth(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + const char *proxyUri = "socks5h://user:pass@proxy.example.com"; + Uri uri(aws_byte_cursor_from_c_str(proxyUri), allocator); + ASSERT_TRUE(uri); + + auto options = Socks5ProxyOptions::CreateFromUri(uri, 0, allocator); + ASSERT_TRUE(options.has_value()); + + const aws_socks5_proxy_options *raw = options->GetUnderlyingHandle(); + ASSERT_NOT_NULL(raw); + ASSERT_NOT_NULL(raw->host); + ASSERT_STR_EQUALS("proxy.example.com", aws_string_c_str(raw->host)); + /* default port */ + ASSERT_INT_EQUALS(1080, raw->port); + ASSERT_UINT_EQUALS(0, raw->connection_timeout_ms); + ASSERT_NOT_NULL(raw->username); + ASSERT_NOT_NULL(raw->password); + ASSERT_STR_EQUALS("user", aws_string_c_str(raw->username)); + ASSERT_STR_EQUALS("pass", aws_string_c_str(raw->password)); + ASSERT_TRUE(*options); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options->LastError()); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5AuthMethod::UsernamePassword), static_cast(options->GetAuthMethod())); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Proxy), static_cast(options->GetHostResolutionMode())); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Proxy), static_cast(options->GetResolutionMode())); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsCreateFromUriAuth, s_TestSocks5ProxyOptionsCreateFromUriAuth) + +static int s_TestSocks5ProxyOptionsCreateFromUriInvalid(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + const char *proxyUri = "http://proxy.example.com:1080"; + Uri uri(aws_byte_cursor_from_c_str(proxyUri), allocator); + ASSERT_TRUE(uri); + + auto options = Socks5ProxyOptions::CreateFromUri(uri, 1000, allocator); + ASSERT_FALSE(options.has_value()); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, aws_last_error()); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsCreateFromUriInvalid, s_TestSocks5ProxyOptionsCreateFromUriInvalid) + +static int s_TestSocks5ProxyOptionsCtorDefaults(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + Socks5ProxyOptions options("proxy.example.com"); + + ASSERT_TRUE(options); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options.LastError()); + ASSERT_INT_EQUALS( + static_cast(Socks5ProxyOptions::DefaultProxyPort), static_cast(options.GetPort())); + ASSERT_INT_EQUALS(static_cast(AwsSocks5AuthMethod::None), static_cast(options.GetAuthMethod())); + ASSERT_FALSE(options.GetUsername().has_value()); + ASSERT_FALSE(options.GetPassword().has_value()); + ASSERT_UINT_EQUALS(0, options.GetConnectionTimeoutMs()); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Proxy), static_cast(options.GetResolutionMode())); + + const aws_socks5_proxy_options *raw = options.GetUnderlyingHandle(); + ASSERT_NOT_NULL(raw); + ASSERT_NOT_NULL(raw->host); + ASSERT_STR_EQUALS("proxy.example.com", aws_string_c_str(raw->host)); + ASSERT_INT_EQUALS(Socks5ProxyOptions::DefaultProxyPort, raw->port); + ASSERT_TRUE(raw->username == NULL); + ASSERT_TRUE(raw->password == NULL); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsCtorDefaults, s_TestSocks5ProxyOptionsCtorDefaults) + +static int s_TestSocks5ProxyOptionsIgnoreCredentialsWhenAuthNone(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + Socks5ProxyAuthConfig authConfig = Socks5ProxyAuthConfig::CreateNone(); + Socks5ProxyOptions options( + "proxy.example.com", + 1080, + authConfig, + 1000, + AwsSocks5HostResolutionMode::Proxy, + allocator); + + ASSERT_TRUE(options); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options.LastError()); + ASSERT_INT_EQUALS(static_cast(AwsSocks5AuthMethod::None), static_cast(options.GetAuthMethod())); + ASSERT_FALSE(options.GetUsername().has_value()); + ASSERT_FALSE(options.GetPassword().has_value()); + + const aws_socks5_proxy_options *raw = options.GetUnderlyingHandle(); + ASSERT_NOT_NULL(raw); + ASSERT_TRUE(raw->username == NULL); + ASSERT_TRUE(raw->password == NULL); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsIgnoreCredentialsWhenAuthNone, s_TestSocks5ProxyOptionsIgnoreCredentialsWhenAuthNone) + +static int s_TestSocks5ProxyOptionsCopyAndMove(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + Socks5ProxyAuthConfig authConfig = Socks5ProxyAuthConfig::CreateUsernamePassword("user", "pass"); + Socks5ProxyOptions original( + "proxy.example.com", + 1080, + authConfig, + 2500, + AwsSocks5HostResolutionMode::Proxy, + allocator); + + ASSERT_TRUE(original); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, original.LastError()); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5AuthMethod::UsernamePassword), static_cast(original.GetAuthMethod())); + + const aws_socks5_proxy_options *rawOriginal = original.GetUnderlyingHandle(); + ASSERT_NOT_NULL(rawOriginal); + ASSERT_NOT_NULL(rawOriginal->username); + ASSERT_NOT_NULL(rawOriginal->password); + + Socks5ProxyOptions copy(original); + const aws_socks5_proxy_options *rawCopy = copy.GetUnderlyingHandle(); + ASSERT_NOT_NULL(rawCopy); + ASSERT_NOT_NULL(rawCopy->username); + ASSERT_NOT_NULL(rawCopy->password); + ASSERT_TRUE(copy); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, copy.LastError()); + /* Deep copy should allocate distinct aws_string instances. */ + ASSERT_TRUE(rawOriginal->username != rawCopy->username); + ASSERT_TRUE(rawOriginal->password != rawCopy->password); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Proxy), static_cast(copy.GetHostResolutionMode())); + + original.SetHostResolutionMode(AwsSocks5HostResolutionMode::Client); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Client), static_cast(original.GetHostResolutionMode())); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Client), static_cast(original.GetResolutionMode())); + /* Copy must remain unchanged. */ + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Proxy), static_cast(copy.GetHostResolutionMode())); + ASSERT_INT_EQUALS(static_cast(AwsSocks5HostResolutionMode::Proxy), static_cast(copy.GetResolutionMode())); + + Socks5ProxyOptions moved(std::move(original)); + const aws_socks5_proxy_options *rawMoved = moved.GetUnderlyingHandle(); + ASSERT_NOT_NULL(rawMoved); + ASSERT_NOT_NULL(rawMoved->host); + ASSERT_STR_EQUALS("proxy.example.com", aws_string_c_str(rawMoved->host)); + ASSERT_INT_EQUALS(1080, rawMoved->port); + ASSERT_TRUE(moved); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, moved.LastError()); + + const aws_socks5_proxy_options *rawOriginalAfterMove = original.GetUnderlyingHandle(); + ASSERT_NOT_NULL(rawOriginalAfterMove); + ASSERT_TRUE(rawOriginalAfterMove->host == NULL); + ASSERT_TRUE(rawOriginalAfterMove->username == NULL); + ASSERT_TRUE(rawOriginalAfterMove->password == NULL); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsCopyAndMove, s_TestSocks5ProxyOptionsCopyAndMove) + +static int s_TestSocks5ProxyOptionsSetters(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + Socks5ProxyOptions options; + ASSERT_FALSE(options); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options.LastError()); + + options.SetConnectionTimeoutMs(1234); + ASSERT_UINT_EQUALS(1234, options.GetConnectionTimeoutMs()); + + ASSERT_TRUE(options.SetProxyEndpoint("proxy.example.com", 1080)); + ASSERT_TRUE(options); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options.LastError()); + auto hostOpt = options.GetHost(); + ASSERT_TRUE(hostOpt.has_value()); + ASSERT_STR_EQUALS("proxy.example.com", hostOpt->c_str()); + ASSERT_INT_EQUALS(1080, options.GetPort()); + + options.SetHostResolutionMode(AwsSocks5HostResolutionMode::Client); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Client), static_cast(options.GetResolutionMode())); + + ASSERT_TRUE(options.SetAuthCredentials("user", "pass")); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5AuthMethod::UsernamePassword), static_cast(options.GetAuthMethod())); + auto usernameOpt = options.GetUsername(); + auto passwordOpt = options.GetPassword(); + ASSERT_TRUE(usernameOpt.has_value()); + ASSERT_TRUE(passwordOpt.has_value()); + ASSERT_STR_EQUALS("user", usernameOpt->c_str()); + ASSERT_STR_EQUALS("pass", passwordOpt->c_str()); + + options.SetConnectionTimeoutMs(4321); + ASSERT_UINT_EQUALS(4321, options.GetConnectionTimeoutMs()); + + ASSERT_TRUE(options.SetProxyEndpoint("new.proxy.local", 1090)); + hostOpt = options.GetHost(); + ASSERT_TRUE(hostOpt.has_value()); + ASSERT_STR_EQUALS("new.proxy.local", hostOpt->c_str()); + ASSERT_INT_EQUALS(1090, options.GetPort()); + ASSERT_UINT_EQUALS(4321, options.GetConnectionTimeoutMs()); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5HostResolutionMode::Client), static_cast(options.GetResolutionMode())); + usernameOpt = options.GetUsername(); + passwordOpt = options.GetPassword(); + ASSERT_TRUE(usernameOpt.has_value()); + ASSERT_TRUE(passwordOpt.has_value()); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5AuthMethod::UsernamePassword), static_cast(options.GetAuthMethod())); + + ASSERT_FALSE(options.SetAuthCredentials("user", "")); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, options.LastError()); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5AuthMethod::UsernamePassword), static_cast(options.GetAuthMethod())); + + ASSERT_FALSE(options.SetProxyEndpoint("", 1090)); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, options.LastError()); + hostOpt = options.GetHost(); + ASSERT_TRUE(hostOpt.has_value()); + ASSERT_STR_EQUALS("new.proxy.local", hostOpt->c_str()); + ASSERT_INT_EQUALS(1090, options.GetPort()); + + ASSERT_FALSE(options.SetProxyEndpoint("overflow.example.com", static_cast(UINT16_MAX) + 1u)); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, options.LastError()); + hostOpt = options.GetHost(); + ASSERT_TRUE(hostOpt.has_value()); + ASSERT_STR_EQUALS("new.proxy.local", hostOpt->c_str()); + ASSERT_INT_EQUALS(1090, options.GetPort()); + + options.ClearAuthCredentials(); + ASSERT_INT_EQUALS(static_cast(AwsSocks5AuthMethod::None), static_cast(options.GetAuthMethod())); + ASSERT_FALSE(options.GetUsername().has_value()); + ASSERT_FALSE(options.GetPassword().has_value()); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options.LastError()); + + ASSERT_TRUE(options.SetProxyEndpoint("noauth.proxy.local", 1105)); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options.LastError()); + auto hostOptAfterClear = options.GetHost(); + ASSERT_TRUE(hostOptAfterClear.has_value()); + ASSERT_INT_EQUALS(static_cast(strlen("noauth.proxy.local")), static_cast(hostOptAfterClear->length())); + ASSERT_INT_EQUALS(1105, options.GetPort()); + ASSERT_INT_EQUALS(static_cast(AwsSocks5AuthMethod::None), static_cast(options.GetAuthMethod())); + ASSERT_FALSE(options.GetUsername().has_value()); + ASSERT_FALSE(options.GetPassword().has_value()); + const aws_socks5_proxy_options *rawAfterClear = options.GetUnderlyingHandle(); + ASSERT_NOT_NULL(rawAfterClear); + ASSERT_NOT_NULL(rawAfterClear->host); + ASSERT_INT_EQUALS(strlen("noauth.proxy.local"), rawAfterClear->host->len); + ASSERT_BIN_ARRAYS_EQUALS( + "noauth.proxy.local", strlen("noauth.proxy.local"), rawAfterClear->host->bytes, rawAfterClear->host->len); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsSetters, s_TestSocks5ProxyOptionsSetters) + +static int s_TestSocks5ProxyOptionsAuthConfig(struct aws_allocator *allocator, void *) +{ + ApiHandle apiHandle(allocator); + + Socks5ProxyOptions options; + ASSERT_TRUE(options.SetProxyEndpoint("auth.proxy.local", 1085)); + ASSERT_TRUE(options); + + auto usernamePasswordConfig = Socks5ProxyAuthConfig::CreateUsernamePassword("userA", "passA"); + ASSERT_TRUE(options.SetAuth(usernamePasswordConfig)); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5AuthMethod::UsernamePassword), static_cast(options.GetAuthMethod())); + auto usernameOpt = options.GetUsername(); + auto passwordOpt = options.GetPassword(); + ASSERT_TRUE(usernameOpt.has_value()); + ASSERT_TRUE(passwordOpt.has_value()); + ASSERT_STR_EQUALS("userA", usernameOpt->c_str()); + ASSERT_STR_EQUALS("passA", passwordOpt->c_str()); + + Socks5ProxyAuthConfig invalidNoneConfig; + invalidNoneConfig.Method = AwsSocks5AuthMethod::None; + invalidNoneConfig.Username = String("should-fail"); + ASSERT_FALSE(options.SetAuth(invalidNoneConfig)); + ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, options.LastError()); + ASSERT_INT_EQUALS( + static_cast(AwsSocks5AuthMethod::UsernamePassword), static_cast(options.GetAuthMethod())); + + Socks5ProxyAuthConfig clearedConfig = Socks5ProxyAuthConfig::CreateNone(); + ASSERT_TRUE(options.SetAuth(clearedConfig)); + ASSERT_INT_EQUALS(static_cast(AwsSocks5AuthMethod::None), static_cast(options.GetAuthMethod())); + ASSERT_FALSE(options.GetUsername().has_value()); + ASSERT_FALSE(options.GetPassword().has_value()); + ASSERT_INT_EQUALS(AWS_ERROR_SUCCESS, options.LastError()); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(Socks5ProxyOptionsAuthConfig, s_TestSocks5ProxyOptionsAuthConfig)