diff --git a/CMakeLists.txt b/CMakeLists.txt index 000d428b5..d4e4442f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,5 +84,6 @@ if (NOT BYO_CRYPTO AND BUILD_TESTING) if (NOT CMAKE_CROSSCOMPILING) add_subdirectory(bin/elasticurl) add_subdirectory(bin/h2benchmark) + add_subdirectory(bin/http_client_app) endif() endif() diff --git a/bin/elasticurl/main.c b/bin/elasticurl/main.c index bedcb7ba0..6fae76201 100644 --- a/bin/elasticurl/main.c +++ b/bin/elasticurl/main.c @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -22,8 +23,13 @@ #include #include #include +#include +#include #include +#include +#include +#include #ifdef _MSC_VER # pragma warning(disable : 4996) /* Disable warnings about fopen() being insecure */ @@ -33,6 +39,120 @@ #define ELASTICURL_VERSION "0.2.0" +struct socks5_proxy_settings { + char *host; + char *username; + char *password; + uint16_t port; + bool resolve_host_with_proxy; +}; + +static void s_socks5_proxy_settings_clean_up( + struct socks5_proxy_settings *settings, + struct aws_allocator *allocator) { + if (!settings) { + return; + } + if (settings->host) { + aws_mem_release(allocator, settings->host); + } + if (settings->username) { + aws_mem_release(allocator, settings->username); + } + if (settings->password) { + aws_mem_release(allocator, settings->password); + } + AWS_ZERO_STRUCT(*settings); +} + +static int s_socks5_proxy_settings_init_from_uri( + struct socks5_proxy_settings *settings, + struct aws_allocator *allocator, + const char *proxy_uri) { + + if (!settings || !allocator || !proxy_uri) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + s_socks5_proxy_settings_clean_up(settings, allocator); + + struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(proxy_uri); + struct aws_uri uri; + AWS_ZERO_STRUCT(uri); + + if (aws_uri_init_parse(&uri, allocator, &uri_cursor)) { + fprintf(stderr, "Failed to parse proxy URI \"%s\": %s\n", proxy_uri, aws_error_debug_str(aws_last_error())); + goto on_error; + } + + const struct aws_byte_cursor *scheme = aws_uri_scheme(&uri); + if (!scheme || !scheme->len) { + fprintf(stderr, "Proxy URI \"%s\" must include scheme socks5h://\n", proxy_uri); + goto on_error; + } + + if (aws_byte_cursor_eq_c_str_ignore_case(scheme, "socks5h")) { + settings->resolve_host_with_proxy = true; + } else if (aws_byte_cursor_eq_c_str_ignore_case(scheme, "socks5")) { + settings->resolve_host_with_proxy = false; + } else { + fprintf(stderr, "Unsupported proxy scheme in \"%s\". Expected socks5h://\n", proxy_uri); + goto on_error; + } + + const struct aws_byte_cursor *host = aws_uri_host_name(&uri); + if (!host || host->len == 0) { + fprintf(stderr, "Proxy URI \"%s\" must include a host\n", proxy_uri); + goto on_error; + } + + settings->host = aws_mem_calloc(allocator, host->len + 1, sizeof(char)); + if (!settings->host) { + fprintf(stderr, "Failed to allocate memory for proxy host\n"); + goto on_error; + } + memcpy(settings->host, host->ptr, host->len); + settings->host[host->len] = '\0'; + + uint32_t parsed_port = aws_uri_port(&uri); + if (parsed_port == 0) { + parsed_port = 1080; + } + if (parsed_port > UINT16_MAX) { + fprintf(stderr, "Proxy port %" PRIu32 " exceeds uint16_t range\n", parsed_port); + goto on_error; + } + settings->port = (uint16_t)parsed_port; + + if (uri.user.len > 0) { + settings->username = aws_mem_calloc(allocator, uri.user.len + 1, sizeof(char)); + if (!settings->username) { + fprintf(stderr, "Failed to allocate memory for proxy username\n"); + goto on_error; + } + memcpy(settings->username, uri.user.ptr, uri.user.len); + settings->username[uri.user.len] = '\0'; + } + + if (uri.password.len > 0) { + settings->password = aws_mem_calloc(allocator, uri.password.len + 1, sizeof(char)); + if (!settings->password) { + fprintf(stderr, "Failed to allocate memory for proxy password\n"); + goto on_error; + } + memcpy(settings->password, uri.password.ptr, uri.password.len); + settings->password[uri.password.len] = '\0'; + } + + aws_uri_clean_up(&uri); + return AWS_OP_SUCCESS; + +on_error: + aws_uri_clean_up(&uri); + s_socks5_proxy_settings_clean_up(settings, allocator); + return AWS_OP_ERR; +} + struct elasticurl_ctx { struct aws_allocator *allocator; const char *verb; @@ -64,6 +184,8 @@ struct elasticurl_ctx { enum aws_log_level log_level; enum aws_http_version required_http_version; bool exchange_completed; + struct socks5_proxy_settings proxy; + bool use_proxy; }; static void s_usage(int exit_code) { @@ -76,6 +198,7 @@ static void s_usage(int exit_code) { fprintf(stderr, " --cert FILE: path to a PEM encoded certificate to use with mTLS\n"); fprintf(stderr, " --key FILE: Path to a PEM encoded private key that matches cert.\n"); fprintf(stderr, " --connect-timeout INT: time in milliseconds to wait for a connection.\n"); + fprintf(stderr, " --proxy URL: SOCKS5 proxy URI (socks5h://... for proxy DNS, socks5://... for local DNS)\n"); fprintf(stderr, " -H, --header LINE: line to send as a header in format [header-key]: [header-value]\n"); fprintf(stderr, " -d, --data STRING: Data to POST or PUT\n"); fprintf(stderr, " --data-file FILE: File to read from file and POST or PUT\n"); @@ -107,6 +230,7 @@ static struct aws_cli_option s_long_options[] = { {"cert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'c'}, {"key", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'e'}, {"connect-timeout", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'f'}, + {"proxy", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'x'}, {"header", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'H'}, {"data", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'd'}, {"data-file", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'g'}, @@ -162,7 +286,7 @@ static void s_parse_options(int argc, char **argv, struct elasticurl_ctx *ctx) { while (true) { int option_index = 0; int c = - aws_cli_getopt_long(argc, argv, "a:b:c:e:f:H:d:g:j:l:m:M:GPHiko:t:v:VwWh", s_long_options, &option_index); + aws_cli_getopt_long(argc, argv, "a:b:c:e:f:H:d:g:j:l:m:M:GPHiko:t:v:VwWhx:", s_long_options, &option_index); if (c == -1) { break; } @@ -186,6 +310,12 @@ static void s_parse_options(int argc, char **argv, struct elasticurl_ctx *ctx) { case 'f': ctx->connect_timeout = atoi(aws_cli_optarg); break; + case 'x': + if (s_socks5_proxy_settings_init_from_uri(&ctx->proxy, ctx->allocator, aws_cli_optarg)) { + s_usage(1); + } + ctx->use_proxy = true; + break; case 'H': if (ctx->header_line_count >= sizeof(ctx->header_lines) / sizeof(const char *)) { fprintf(stderr, "currently only 10 header lines are supported.\n"); @@ -618,6 +748,9 @@ int main(int argc, char **argv) { struct aws_tls_connection_options tls_connection_options; AWS_ZERO_STRUCT(tls_connection_options); struct aws_tls_connection_options *tls_options = NULL; + bool socks5_options_valid = false; + struct aws_socks5_proxy_options socks5_options; + AWS_ZERO_STRUCT(socks5_options); if (use_tls) { if (app_ctx.cert && app_ctx.key) { @@ -710,6 +843,40 @@ int main(int argc, char **argv) { .keep_alive_interval_sec = 0, }; + if (app_ctx.use_proxy) { + if (!app_ctx.proxy.host) { + fprintf(stderr, "Proxy URI was requested but no host was parsed.\n"); + exit(1); + } + + struct aws_byte_cursor proxy_host = aws_byte_cursor_from_c_str(app_ctx.proxy.host); + if (aws_socks5_proxy_options_init(&socks5_options, allocator, proxy_host, app_ctx.proxy.port) != AWS_OP_SUCCESS) { + fprintf( + stderr, + "Failed to initialize SOCKS5 proxy options: %s\n", + aws_error_debug_str(aws_last_error())); + exit(1); + } + aws_socks5_proxy_options_set_host_resolution_mode( + &socks5_options, + app_ctx.proxy.resolve_host_with_proxy ? AWS_SOCKS5_HOST_RESOLUTION_PROXY + : AWS_SOCKS5_HOST_RESOLUTION_CLIENT); + + if (app_ctx.proxy.username && app_ctx.proxy.password) { + struct aws_byte_cursor username = aws_byte_cursor_from_c_str(app_ctx.proxy.username); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str(app_ctx.proxy.password); + if (aws_socks5_proxy_options_set_auth(&socks5_options, allocator, username, password) != AWS_OP_SUCCESS) { + fprintf( + stderr, + "Failed to set SOCKS5 auth: %s\n", + aws_error_debug_str(aws_last_error())); + exit(1); + } + } + + socks5_options_valid = true; + } + struct aws_http_client_connection_options http_client_options = { .self_size = sizeof(struct aws_http_client_connection_options), .socket_options = &socket_options, @@ -719,6 +886,7 @@ int main(int argc, char **argv) { .bootstrap = bootstrap, .initial_window_size = SIZE_MAX, .tls_options = tls_options, + .socks5_proxy_options = socks5_options_valid ? &socks5_options : NULL, .user_data = &app_ctx, .on_setup = s_on_client_connection_setup, .on_shutdown = s_on_client_connection_shutdown, @@ -732,6 +900,10 @@ int main(int argc, char **argv) { aws_condition_variable_wait_pred(&app_ctx.c_var, &app_ctx.mutex, s_completion_predicate, &app_ctx); aws_mutex_unlock(&app_ctx.mutex); + if (socks5_options_valid) { + aws_socks5_proxy_options_clean_up(&socks5_options); + } + aws_client_bootstrap_release(bootstrap); aws_host_resolver_release(resolver); aws_event_loop_group_release(el_group); @@ -748,6 +920,7 @@ int main(int argc, char **argv) { aws_logger_clean_up(&logger); } + s_socks5_proxy_settings_clean_up(&app_ctx.proxy, app_ctx.allocator); aws_uri_clean_up(&app_ctx.uri); aws_http_message_destroy(app_ctx.request); diff --git a/bin/http_client_app/CMakeLists.txt b/bin/http_client_app/CMakeLists.txt new file mode 100644 index 000000000..063f89cec --- /dev/null +++ b/bin/http_client_app/CMakeLists.txt @@ -0,0 +1,28 @@ +project(http_client_c_socks5_app C) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_PREFIX_PATH}/lib/cmake") + +file(GLOB HTTP_CLIENT_APP_SRC + "*.c" + ) + +set(HTTP_CLIENT_APP_PROJECT_NAME http_client_app) +add_executable(${HTTP_CLIENT_APP_PROJECT_NAME} ${HTTP_CLIENT_APP_SRC}) +aws_set_common_properties(${HTTP_CLIENT_APP_PROJECT_NAME}) + +target_include_directories(${HTTP_CLIENT_APP_PROJECT_NAME} PUBLIC + $ + $) + +target_link_libraries(${HTTP_CLIENT_APP_PROJECT_NAME} PRIVATE aws-c-http) + +if (BUILD_SHARED_LIBS AND NOT WIN32) + message(INFO " http client 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 ${HTTP_CLIENT_APP_PROJECT_NAME} + EXPORT ${HTTP_CLIENT_APP_PROJECT_NAME}-targets + COMPONENT Runtime + RUNTIME + DESTINATION bin + COMPONENT Runtime) \ No newline at end of file diff --git a/bin/http_client_app/main.c b/bin/http_client_app/main.c new file mode 100644 index 000000000..d2cceb9da --- /dev/null +++ b/bin/http_client_app/main.c @@ -0,0 +1,828 @@ +/** + * 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 + +#include +#include +#include +#include +#include + +struct socks5_proxy_settings { + char *host; + char *username; + char *password; + uint16_t port; + bool resolve_host_with_proxy; +}; + +static void s_socks5_proxy_settings_clean_up( + struct socks5_proxy_settings *settings, + struct aws_allocator *allocator) { + if (!settings) { + return; + } + if (settings->host) { + aws_mem_release(allocator, settings->host); + } + if (settings->username) { + aws_mem_release(allocator, settings->username); + } + if (settings->password) { + aws_mem_release(allocator, settings->password); + } + AWS_ZERO_STRUCT(*settings); +} + +static int s_socks5_proxy_settings_init_from_uri( + struct socks5_proxy_settings *settings, + struct aws_allocator *allocator, + const char *proxy_uri) { + + if (!settings || !allocator || !proxy_uri) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + /* Clean up any existing data before reusing the struct */ + s_socks5_proxy_settings_clean_up(settings, allocator); + + struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(proxy_uri); + struct aws_uri uri; + AWS_ZERO_STRUCT(uri); + + if (aws_uri_init_parse(&uri, allocator, &uri_cursor)) { + fprintf(stderr, "Failed to parse proxy URI \"%s\": %s\n", proxy_uri, aws_error_debug_str(aws_last_error())); + goto on_error; + } + + const struct aws_byte_cursor *scheme = aws_uri_scheme(&uri); + if (!scheme || !scheme->len) { + fprintf(stderr, "Proxy URI \"%s\" must include scheme socks5h://\n", proxy_uri); + goto on_error; + } + + if (aws_byte_cursor_eq_c_str_ignore_case(scheme, "socks5h")) { + settings->resolve_host_with_proxy = true; + } else if (aws_byte_cursor_eq_c_str_ignore_case(scheme, "socks5")) { + settings->resolve_host_with_proxy = false; + } else { + fprintf(stderr, "Unsupported proxy scheme in \"%s\". Expected socks5h://\n", proxy_uri); + goto on_error; + } + + const struct aws_byte_cursor *host = aws_uri_host_name(&uri); + if (!host || host->len == 0) { + fprintf(stderr, "Proxy URI \"%s\" must include a host\n", proxy_uri); + goto on_error; + } + + settings->host = aws_mem_calloc(allocator, host->len + 1, sizeof(char)); + if (!settings->host) { + fprintf(stderr, "Failed to allocate memory for proxy host\n"); + goto on_error; + } + memcpy(settings->host, host->ptr, host->len); + settings->host[host->len] = '\0'; + + uint32_t parsed_port = aws_uri_port(&uri); + if (parsed_port == 0) { + parsed_port = 1080; + } + if (parsed_port > UINT16_MAX) { + fprintf(stderr, "Proxy port %" PRIu32 " exceeds uint16_t range\n", parsed_port); + goto on_error; + } + settings->port = (uint16_t)parsed_port; + + if (uri.user.len > 0) { + settings->username = aws_mem_calloc(allocator, uri.user.len + 1, sizeof(char)); + if (!settings->username) { + fprintf(stderr, "Failed to allocate memory for proxy username\n"); + goto on_error; + } + memcpy(settings->username, uri.user.ptr, uri.user.len); + settings->username[uri.user.len] = '\0'; + } + + if (uri.password.len > 0) { + settings->password = aws_mem_calloc(allocator, uri.password.len + 1, sizeof(char)); + if (!settings->password) { + fprintf(stderr, "Failed to allocate memory for proxy password\n"); + goto on_error; + } + memcpy(settings->password, uri.password.ptr, uri.password.len); + settings->password[uri.password.len] = '\0'; + } + + aws_uri_clean_up(&uri); + return AWS_OP_SUCCESS; + +on_error: + aws_uri_clean_up(&uri); + s_socks5_proxy_settings_clean_up(settings, allocator); + return AWS_OP_ERR; +} + +/* + * This example demonstrates how to make HTTP requests through a SOCKS5 proxy + * or directly to the server. + * + * It supports both modes of operation and can be used to test/compare + * connectivity with and without the proxy. + */ + +struct app_ctx { + struct aws_allocator *allocator; + struct aws_event_loop_group *event_loop_group; + struct aws_host_resolver *host_resolver; + struct aws_client_bootstrap *bootstrap; + + struct aws_http_connection *connection; + struct aws_http_message *request; + struct aws_http_stream *stream; + + struct aws_mutex lock; + struct aws_condition_variable signal; + + const char *host_name; + const char *target_ip; + uint16_t port; + const char *path; + struct socks5_proxy_settings proxy; + + bool use_proxy; + bool use_tls; + bool verbose; + + bool connection_complete; + int connection_error_code; + bool stream_complete; + int stream_error_code; + + int response_status; + struct aws_byte_buf response_body; + + /* HTTP Connection Monitoring */ + struct aws_atomic_var pending_requests; +}; + +static void s_usage(int exit_code) { + fprintf(stderr, "usage: http_socks5_example [options]\n"); + fprintf(stderr, " --host HOST: Target HTTP host to connect to (default: example.com)\n"); + fprintf(stderr, " --target-ip IP: Target IP address (overrides --host for connection but Host header uses --host)\n"); + fprintf(stderr, " --port PORT: Target port (default: 80 for HTTP, 443 for HTTPS)\n"); + fprintf(stderr, " --path PATH: HTTP request path (default: /)\n"); + fprintf( + stderr, + " --proxy URL: SOCKS5 proxy URI (socks5h://... for proxy DNS, socks5://... for local DNS)\n"); + fprintf(stderr, " TLS is used automatically when the port is set to 443 or 8443\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[] = { + {"host", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'h'}, + {"target-ip", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'i'}, + {"port", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'p'}, + {"path", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'a'}, + {"proxy", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'x'}, + {"verbose", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'v'}, + {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'H'}, + {NULL, 0, NULL, 0}, +}; + +static void s_parse_options(int argc, char **argv, struct app_ctx *ctx) { + ctx->host_name = "example.com"; + ctx->target_ip = NULL; /* NULL means use host_name for connection */ + ctx->port = 0; /* Will be set later based on whether TLS is enabled */ + ctx->path = "/"; + ctx->use_proxy = false; + ctx->use_tls = false; + ctx->verbose = false; + + while (true) { + int option_index = 0; + int c = aws_cli_getopt_long(argc, argv, "h:i:p:a:x:vH", s_long_options, &option_index); + if (c == -1) { + break; + } + + switch (c) { + case 'h': + ctx->host_name = aws_cli_optarg; + break; + case 'i': + ctx->target_ip = aws_cli_optarg; + break; + case 'p': + ctx->port = (uint16_t)atoi(aws_cli_optarg); + break; + case 'a': + ctx->path = aws_cli_optarg; + break; + case 'x': + if (s_socks5_proxy_settings_init_from_uri(&ctx->proxy, ctx->allocator, aws_cli_optarg)) { + s_usage(1); + } + ctx->use_proxy = true; + break; + case 'v': + ctx->verbose = true; + break; + case 'H': + s_usage(0); + break; + default: + fprintf(stderr, "Unknown option: %c\n", c); + s_usage(1); + break; + } + } + + /* Set default port if not specified */ + if (ctx->port == 0) { + ctx->port = 80; + } + ctx->use_tls = (ctx->port == 443 || ctx->port == 8443); +} + +/* Predicate functions for condition variables */ +static bool s_connection_completed_predicate(void *arg) { + struct app_ctx *ctx = arg; + return ctx->connection_complete; +} + +static bool s_stream_completed_predicate(void *arg) { + struct app_ctx *ctx = arg; + return ctx->stream_complete; +} + +static void s_app_ctx_init(struct app_ctx *ctx, struct aws_allocator *allocator) { + AWS_ZERO_STRUCT(*ctx); + ctx->allocator = allocator; + aws_mutex_init(&ctx->lock); + aws_condition_variable_init(&ctx->signal); + aws_atomic_init_int(&ctx->pending_requests, 0); + aws_byte_buf_init(&ctx->response_body, allocator, 1024); /* Initial buffer capacity */ +} + +static void s_app_ctx_clean_up(struct app_ctx *ctx) { + if (ctx->stream) { + aws_http_stream_release(ctx->stream); + } + + if (ctx->request) { + aws_http_message_destroy(ctx->request); + } + + if (ctx->connection) { + aws_http_connection_release(ctx->connection); + } + + if (ctx->bootstrap) { + aws_client_bootstrap_release(ctx->bootstrap); + } + + if (ctx->host_resolver) { + aws_host_resolver_release(ctx->host_resolver); + } + + if (ctx->event_loop_group) { + aws_event_loop_group_release(ctx->event_loop_group); + } + + aws_byte_buf_clean_up(&ctx->response_body); + s_socks5_proxy_settings_clean_up(&ctx->proxy, ctx->allocator); + aws_condition_variable_clean_up(&ctx->signal); + aws_mutex_clean_up(&ctx->lock); +} + +/* Callback for HTTP connection setup */ +static void s_on_connection_setup(struct aws_http_connection *connection, int error_code, void *user_data) { + struct app_ctx *ctx = user_data; + + aws_mutex_lock(&ctx->lock); + + ctx->connection_complete = true; + ctx->connection_error_code = error_code; + + if (error_code == AWS_ERROR_SUCCESS) { + ctx->connection = connection; + if (ctx->verbose) { + printf("HTTP connection established successfully\n"); + } + } else { + fprintf(stderr, "HTTP connection failed: %s\n", aws_error_debug_str(error_code)); + } + + aws_condition_variable_notify_one(&ctx->signal); + aws_mutex_unlock(&ctx->lock); +} + +/* Callback for HTTP connection shutdown */ +static void s_on_connection_shutdown(struct aws_http_connection *connection, int error_code, void *user_data) { + struct app_ctx *ctx = user_data; + + if (error_code != AWS_ERROR_SUCCESS && ctx->verbose) { + fprintf(stderr, "HTTP connection shutdown with error: %s\n", aws_error_debug_str(error_code)); + } else if (ctx->verbose) { + printf("HTTP connection closed successfully\n"); + } + + /* No need to signal since we'll wait for stream completion instead */ +} + +/* Callback for HTTP stream completion */ +static void s_on_stream_complete(struct aws_http_stream *stream, int error_code, void *user_data) { + struct app_ctx *ctx = user_data; + + aws_mutex_lock(&ctx->lock); + + ctx->stream_complete = true; + ctx->stream_error_code = error_code; + + if (error_code != AWS_ERROR_SUCCESS) { + fprintf(stderr, "HTTP stream failed: %s\n", aws_error_debug_str(error_code)); + } else if (ctx->verbose) { + printf("HTTP stream completed successfully\n"); + } + + aws_atomic_fetch_sub(&ctx->pending_requests, 1); + aws_condition_variable_notify_one(&ctx->signal); + aws_mutex_unlock(&ctx->lock); +} + +/* Callback for HTTP stream header block completion */ +static int s_on_response_headers(struct aws_http_stream *stream, enum aws_http_header_block header_block, const struct aws_http_header *header_array, size_t num_headers, void *user_data) { + struct app_ctx *ctx = user_data; + + /* Get response status code */ + aws_http_stream_get_incoming_response_status(stream, &ctx->response_status); + + if (ctx->verbose) { + printf("\nResponse status: %d\n", ctx->response_status); + printf("Headers:\n"); + for (size_t i = 0; i < num_headers; i++) { + printf(" %.*s: %.*s\n", + (int)header_array[i].name.len, header_array[i].name.ptr, + (int)header_array[i].value.len, header_array[i].value.ptr); + } + printf("\n"); + } + + return AWS_OP_SUCCESS; +} + +/* Callback for HTTP stream body data */ +static int s_on_response_body(struct aws_http_stream *stream, const struct aws_byte_cursor *data, void *user_data) { + struct app_ctx *ctx = user_data; + + /* Append response data to our buffer */ + aws_byte_buf_append_dynamic(&ctx->response_body, data); + + /* Print data as it arrives if in verbose mode */ + if (ctx->verbose) { + printf("Received %zu bytes\n", data->len); + } + + return AWS_OP_SUCCESS; +} + +/* Create and send HTTP request */ +static int s_send_http_request(struct app_ctx *ctx) { + /* Create the HTTP request */ + ctx->request = aws_http_message_new_request(ctx->allocator); + + if (!ctx->request) { + fprintf(stderr, "Failed to create HTTP request: %s\n", aws_error_debug_str(aws_last_error())); + return AWS_OP_ERR; + } + + /* Set HTTP method to GET */ + struct aws_byte_cursor method = aws_byte_cursor_from_c_str("GET"); + aws_http_message_set_request_method(ctx->request, method); + + /* Set request path */ + struct aws_byte_cursor path = aws_byte_cursor_from_c_str(ctx->path); + aws_http_message_set_request_path(ctx->request, path); + + /* Add host header */ + struct aws_http_header host_header = { + .name = aws_byte_cursor_from_c_str("Host"), + .value = aws_byte_cursor_from_c_str(ctx->host_name) + }; + aws_http_message_add_header(ctx->request, host_header); + + /* Add user-agent header */ + struct aws_http_header ua_header = { + .name = aws_byte_cursor_from_c_str("User-Agent"), + .value = aws_byte_cursor_from_c_str("aws-crt-http-socks5-example/1.0") + }; + aws_http_message_add_header(ctx->request, ua_header); + + /* Setup stream options */ + struct aws_http_make_request_options request_options = { + .self_size = sizeof(request_options), + .request = ctx->request, + .user_data = ctx, + .on_response_headers = s_on_response_headers, + .on_response_header_block_done = NULL, + .on_response_body = s_on_response_body, + .on_complete = s_on_stream_complete, + }; + + /* Send the request */ + aws_atomic_fetch_add(&ctx->pending_requests, 1); + ctx->stream = aws_http_connection_make_request(ctx->connection, &request_options); + + if (!ctx->stream) { + fprintf(stderr, "Failed to create HTTP stream: %s\n", aws_error_debug_str(aws_last_error())); + aws_atomic_fetch_sub(&ctx->pending_requests, 1); + return AWS_OP_ERR; + } + + /* Activate the stream */ + int result = aws_http_stream_activate(ctx->stream); + if (result) { + fprintf(stderr, "Failed to activate HTTP stream: %s\n", aws_error_debug_str(aws_last_error())); + aws_atomic_fetch_sub(&ctx->pending_requests, 1); + return AWS_OP_ERR; + } + + if (ctx->verbose) { + printf("HTTP request sent to: %s%s\n", ctx->host_name, ctx->path); + } + + return AWS_OP_SUCCESS; +} + +int main(int argc, char **argv) { + int result = 0; + struct aws_allocator *allocator = aws_default_allocator(); + /* Create TLS context options if using TLS */ + struct aws_tls_connection_options *tls_connection_options = NULL; + struct aws_tls_ctx *tls_ctx = NULL; + struct aws_socks5_proxy_options *socks5_options = NULL; + + aws_common_library_init(allocator); + aws_io_library_init(allocator); + aws_http_library_init(allocator); + + struct app_ctx app_ctx; + s_app_ctx_init(&app_ctx, allocator); + + /* Parse command line arguments */ + s_parse_options(argc, argv, &app_ctx); + + // Initialize AWS CRT logger to stderr (or NULL for stdout) + struct aws_logger logger; + struct aws_logger_standard_options logger_options = { + .level = app_ctx.verbose ? AWS_LL_TRACE : AWS_LL_WARN, // Use TRACE for verbose mode + .file = stderr, // Use stderr for logs; NULL for stdout + }; + bool logger_initialized = false; + if (aws_logger_init_standard(&logger, allocator, &logger_options) == AWS_OP_SUCCESS) { + aws_logger_set(&logger); + logger_initialized = true; + + if (app_ctx.verbose) { + printf("Verbose mode enabled, using TRACE log level\n"); + } + } else { + result = AWS_OP_ERR; + fprintf(stderr, "[WARN] Failed to initialize AWS logger, logs will not be shown.\n"); + } + + /* Log the configuration */ + printf("HTTP%s request to %s:%d%s\n", + app_ctx.use_tls ? "S" : "", + app_ctx.host_name, + app_ctx.port, + app_ctx.path); + + if (app_ctx.use_proxy && app_ctx.proxy.host) { + printf("Using SOCKS5 proxy at %s:%" PRIu16 "\n", app_ctx.proxy.host, app_ctx.proxy.port); + if (app_ctx.proxy.username) { + printf("With proxy authentication: username=%s\n", app_ctx.proxy.username); + } + } else { + printf("Using direct connection (no proxy)\n"); + } + + /* Create event loop group */ + app_ctx.event_loop_group = aws_event_loop_group_new_default(allocator, 1, NULL); + if (!app_ctx.event_loop_group) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to create event loop group: %s\n", aws_error_debug_str(aws_last_error())); + goto cleanup; + } + + /* Create host resolver */ + struct aws_host_resolver_default_options resolver_options = { + .max_entries = 8, + .el_group = app_ctx.event_loop_group + }; + app_ctx.host_resolver = aws_host_resolver_new_default(allocator, &resolver_options); + if (!app_ctx.host_resolver) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to create host resolver: %s\n", aws_error_debug_str(aws_last_error())); + goto cleanup; + } + + /* Create client bootstrap */ + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = app_ctx.event_loop_group, + .host_resolver = app_ctx.host_resolver + }; + app_ctx.bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + if (!app_ctx.bootstrap) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to create client bootstrap: %s\n", aws_error_debug_str(aws_last_error())); + goto cleanup; + } + + if (app_ctx.use_tls) { + /* Initialize default TLS context options */ + struct aws_tls_ctx_options tls_ctx_options; + aws_tls_ctx_options_init_default_client(&tls_ctx_options, allocator); + + /* Create a new TLS context */ + tls_ctx = aws_tls_client_ctx_new(allocator, &tls_ctx_options); + if (!tls_ctx) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to create TLS context: %s\n", aws_error_debug_str(aws_last_error())); + aws_tls_ctx_options_clean_up(&tls_ctx_options); + goto cleanup; + } + + /* Initialize TLS connection options */ + tls_connection_options = aws_mem_calloc(allocator, 1, sizeof(struct aws_tls_connection_options)); + if (!tls_connection_options) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to allocate memory for TLS connection options\n"); + aws_tls_ctx_options_clean_up(&tls_ctx_options); + aws_tls_ctx_release(tls_ctx); + goto cleanup; + } + + /* Initialize TLS connection options from context */ + aws_tls_connection_options_init_from_ctx(tls_connection_options, tls_ctx); + + /* Set server name for SNI */ + struct aws_byte_cursor server_name = aws_byte_cursor_from_c_str(app_ctx.host_name); + if (aws_tls_connection_options_set_server_name(tls_connection_options, allocator, + &server_name) != AWS_OP_SUCCESS) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to set server name: %s\n", aws_error_debug_str(aws_last_error())); + goto cleanup; + } + + printf("TLS enabled for connection to %s\n", app_ctx.host_name); + } + + /* Prepare HTTP connection options */ + struct aws_http_client_connection_options http_options; + AWS_ZERO_STRUCT(http_options); + http_options.self_size = sizeof(http_options); + http_options.bootstrap = app_ctx.bootstrap; + http_options.allocator = allocator; + http_options.user_data = &app_ctx; + + http_options.host_name = aws_byte_cursor_from_c_str(app_ctx.host_name); + http_options.port = app_ctx.port; + http_options.on_setup = s_on_connection_setup; + http_options.on_shutdown = s_on_connection_shutdown; + + /* For TLS connections (both direct and through proxy): + * - When using TLS directly, we set the TLS options to connect directly to the target + * - When using TLS through SOCKS5, we'll set TLS options later with the proxy configuration + * to ensure the TLS handshake happens AFTER the SOCKS5 tunnel is established + */ + if (app_ctx.use_tls && !app_ctx.use_proxy) { + /* Set TLS options for direct connections */ + http_options.tls_options = tls_connection_options; + } + + /* Setup socket options for the connection */ + struct aws_socket_options socket_options = { + .type = AWS_SOCKET_STREAM, + .domain = AWS_SOCKET_IPV4, /* Use IPv4 for better compatibility */ + .connect_timeout_ms = 5000, /* Allow enough time for connection */ + }; + + http_options.socket_options = &socket_options; + + /* Configure proxy options if using proxy */ + + if (app_ctx.use_proxy && app_ctx.proxy.host) { + printf("Configuring SOCKS5 proxy %s:%" PRIu16 "\n", app_ctx.proxy.host, app_ctx.proxy.port); + + /* Allocate and initialize the SOCKS5 options structure using standard AWS API */ + socks5_options = aws_mem_calloc(allocator, 1, sizeof(struct aws_socks5_proxy_options)); + if (!socks5_options) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to allocate memory for SOCKS5 proxy options\n"); + goto cleanup; + } + + /* Set up SOCKS5-specific options */ + struct aws_byte_cursor proxy_host = aws_byte_cursor_from_c_str(app_ctx.proxy.host); + + printf("Using proxy host: %s:%" PRIu16 "\n", app_ctx.proxy.host, app_ctx.proxy.port); + + /* Use the standard AWS SOCKS5 initialization function */ + int result = aws_socks5_proxy_options_init(socks5_options, allocator, proxy_host, app_ctx.proxy.port); + if (result != AWS_OP_SUCCESS) { + int error_code = aws_last_error(); + result = error_code; + fprintf(stderr, "Failed to initialize SOCKS5 proxy options: %s (code: %d) result %d\n", + aws_error_debug_str(error_code), error_code, result); + + /* More specific error handling */ + if (error_code == AWS_ERROR_INVALID_ARGUMENT) { + fprintf(stderr, "Invalid argument provided to init function. Check host and port.\n"); + } else { + fprintf(stderr, "Unknown error during SOCKS5 proxy options initialization\n"); + } + + goto cleanup; + } + + aws_socks5_proxy_options_set_host_resolution_mode( + socks5_options, + app_ctx.proxy.resolve_host_with_proxy ? AWS_SOCKS5_HOST_RESOLUTION_PROXY + : AWS_SOCKS5_HOST_RESOLUTION_CLIENT); + + printf( + "Successfully initialized SOCKS5 proxy options (destination resolved by %s)\n", + app_ctx.proxy.resolve_host_with_proxy ? "proxy" : "client"); + + /* Setup auth if provided */ + if (app_ctx.proxy.username && app_ctx.proxy.password) { + struct aws_byte_cursor username = aws_byte_cursor_from_c_str(app_ctx.proxy.username); + struct aws_byte_cursor password = aws_byte_cursor_from_c_str(app_ctx.proxy.password); + if (aws_socks5_proxy_options_set_auth(socks5_options, allocator, username, password) != AWS_OP_SUCCESS) { + int error_code = aws_last_error(); + result = error_code; + fprintf(stderr, "Failed to set SOCKS5 auth: %s (code: %d)\n", + aws_error_debug_str(error_code), error_code); + goto cleanup; + } + } + + /* Use target_ip if specified, otherwise use host_name */ + const char* target_host_str = app_ctx.target_ip ? app_ctx.target_ip : app_ctx.host_name; + + /* + * The helper will detect IPv4/IPv6 literals automatically when we pass DOMAIN here, + * so callers don't need to care about the address family. + * + * Note: With SOCKS5, first the TCP connection is established to the proxy, + * then the SOCKS5 handshake is performed, and finally the TLS handshake + * happens THROUGH the established SOCKS5 tunnel. + */ + + if (app_ctx.verbose) { + fprintf(stdout, "Connecting to %s using SOCKS5 address type: DOMAIN (with automatic IPv4/IPv6 detection)\n", target_host_str); + } + + /* Target endpoint is taken directly from the connection options (host/port). */ + /* Set additional options */ + socks5_options->connection_timeout_ms = 5000; /* 5 seconds */ + + /* Set SOCKS5 options directly in the HTTP connection options */ + http_options.socks5_proxy_options = socks5_options; + + /* When using SOCKS5 with TLS, we need to: + * 1. Connect to the SOCKS5 proxy first (without TLS) + * 2. Perform the SOCKS5 handshake to establish the tunnel + * 3. Then establish TLS through the tunnel to the target server + * + * The AWS HTTP client should handle this sequence correctly when we set + * both SOCKS5 proxy options and TLS options. + */ + if (app_ctx.use_tls) { + if (app_ctx.verbose) { + printf("TLS will be established through SOCKS5 tunnel to %s\n", app_ctx.host_name); + } + + /* CRITICAL: Set TLS options when using TLS with SOCKS5 + * This tells the AWS HTTP client to establish a TLS connection + * AFTER the SOCKS5 tunnel is established */ + http_options.tls_options = tls_connection_options; + + /* Also ensure the server_name is set correctly for SNI */ + struct aws_byte_cursor server_name = aws_byte_cursor_from_c_str(app_ctx.host_name); + if (aws_tls_connection_options_set_server_name(tls_connection_options, allocator, &server_name) != AWS_OP_SUCCESS) { + result = AWS_OP_ERR; + fprintf(stderr, "Failed to set server name for TLS over SOCKS5: %s\n", aws_error_debug_str(aws_last_error())); + } + } + + if (app_ctx.verbose) { + printf("SOCKS5 proxy configured with target %s:%d%s\n", + target_host_str, app_ctx.port, + app_ctx.use_tls ? " (with TLS)" : ""); + } + } + + printf("Starting HTTP connection...\n"); + /* Create HTTP connection */ + if (aws_http_client_connect(&http_options)) { + fprintf(stderr, "Failed to initiate HTTP connection: %s\n", aws_error_debug_str(aws_last_error())); + result = aws_last_error(); + goto cleanup; + } + printf("HTTP connection initiated, waiting for completion...\n"); + + /* Wait for connection completion */ + aws_mutex_lock(&app_ctx.lock); + aws_condition_variable_wait_pred( + &app_ctx.signal, &app_ctx.lock, s_connection_completed_predicate, &app_ctx); + aws_mutex_unlock(&app_ctx.lock); + + if (app_ctx.connection_error_code != AWS_ERROR_SUCCESS) { + fprintf(stderr, "HTTP connection failed: %s\n", aws_error_debug_str(app_ctx.connection_error_code)); + result = -1; + goto cleanup; + } + printf("HTTP connection established, sending request...\n"); + + /* Send HTTP request */ + if (s_send_http_request(&app_ctx) != AWS_OP_SUCCESS) { + result = aws_last_error(); + goto cleanup; + } + + printf("HTTP request sent, waiting for response...\n"); + /* Wait for request to complete */ + aws_mutex_lock(&app_ctx.lock); + aws_condition_variable_wait_pred( + &app_ctx.signal, &app_ctx.lock, s_stream_completed_predicate, &app_ctx); + aws_mutex_unlock(&app_ctx.lock); + + /* Print the results */ + printf("\nHTTP Response Status: %d\n", app_ctx.response_status); + if (app_ctx.stream_error_code == AWS_ERROR_SUCCESS) { + printf("Response Body (%zu bytes):\n", app_ctx.response_body.len); + printf("------------------------------------\n"); + printf("%.*s\n", (int)app_ctx.response_body.len, app_ctx.response_body.buffer); + printf("------------------------------------\n"); + } else { + result = app_ctx.stream_error_code; + fprintf(stderr, "Request failed: %s\n", aws_error_debug_str(app_ctx.stream_error_code)); + } + +cleanup: + /* Cleanup TLS resources if used */ + if (tls_connection_options != NULL) { + aws_tls_connection_options_clean_up(tls_connection_options); + aws_mem_release(allocator, tls_connection_options); + } + + if (tls_ctx != NULL) { + aws_tls_ctx_release(tls_ctx); + } + + /* Clean up proxy options if used */ + if (socks5_options != NULL) { + aws_socks5_proxy_options_clean_up(socks5_options); + aws_mem_release(allocator, socks5_options); + } + + /* Clean up the app context */ + s_app_ctx_clean_up(&app_ctx); + + /* Clean up libraries */ + aws_http_library_clean_up(); + aws_io_library_clean_up(); + aws_common_library_clean_up(); + + /* Clean up logger before exit */ + if (logger_initialized) { + aws_logger_clean_up(&logger); + } + return result; +} diff --git a/include/aws/http/connection.h b/include/aws/http/connection.h index 582fb86e2..203a09324 100644 --- a/include/aws/http/connection.h +++ b/include/aws/http/connection.h @@ -315,6 +315,15 @@ struct aws_http_client_connection_options { */ const struct aws_http_proxy_options *proxy_options; + /** + * Optional + * Configuration options for SOCKS5 proxy. + * Used for SOCKS5 proxy connections which operate at the socket level. + * If this is set, proxy_options will be ignored as they are mutually exclusive. + * Relevant fields are copied internally. + */ + const struct aws_socks5_proxy_options *socks5_proxy_options; + /* * Optional. * Configuration for using proxy from environment variable. diff --git a/include/aws/http/connection_manager.h b/include/aws/http/connection_manager.h index 3a5fe0fe9..6398b16ea 100644 --- a/include/aws/http/connection_manager.h +++ b/include/aws/http/connection_manager.h @@ -19,6 +19,7 @@ struct aws_socket_options; struct aws_tls_connection_options; struct proxy_env_var_settings; struct aws_http2_setting; +struct aws_socks5_proxy_options; typedef void(aws_http_connection_manager_on_connection_setup_fn)( struct aws_http_connection *connection, @@ -102,6 +103,13 @@ struct aws_http_connection_manager_options { /* Proxy configuration for http connection */ const struct aws_http_proxy_options *proxy_options; + /** + * Optional + * Configuration options for SOCKS5 proxy connections. When set, HTTP proxy configuration (including proxy + * environment variables) is ignored and traffic is routed via the SOCKS5 proxy instead. + */ + const struct aws_socks5_proxy_options *socks5_proxy_options; + /* * Optional. * Configuration for using proxy from environment variable. diff --git a/include/aws/http/private/connection_impl.h b/include/aws/http/private/connection_impl.h index fd9c915ab..6fed30c6d 100644 --- a/include/aws/http/private/connection_impl.h +++ b/include/aws/http/private/connection_impl.h @@ -139,6 +139,8 @@ struct aws_http_client_bootstrap { struct aws_http2_connection_options http2_options; /* allocated with bootstrap */ struct aws_hash_table *alpn_string_map; /* allocated with bootstrap */ struct aws_http_connection *connection; + + struct aws_atomic_var destroyed; }; AWS_EXTERN_C_BEGIN diff --git a/include/aws/http/websocket.h b/include/aws/http/websocket.h index 39703b4e2..ce8b83191 100644 --- a/include/aws/http/websocket.h +++ b/include/aws/http/websocket.h @@ -6,6 +6,7 @@ */ #include +#include AWS_PUSH_SANE_WARNING_LEVEL @@ -175,6 +176,13 @@ struct aws_websocket_client_connection_options { */ const struct aws_http_proxy_options *proxy_options; + /** + * Optional + * Configuration options for SOCKS5 proxy usage. + * Mutually exclusive with proxy_options. + */ + const struct aws_socks5_proxy_options *socks5_proxy_options; + /** * Required. * aws_websocket_client_connect() makes a copy. diff --git a/integration-testing/http_client_test.bash b/integration-testing/http_client_test.bash new file mode 100755 index 000000000..70f218292 --- /dev/null +++ b/integration-testing/http_client_test.bash @@ -0,0 +1,95 @@ +#!/bin/bash + +declare -a TEST_NAMES +declare -a TEST_RESULTS +declare -a TEST_CODES + +# Hosts and ports +LOCAL_HOST=localhost +REMOTE_HOST=httpbin.org +PORT_HTTP=80 +PORT_HTTPS=443 +PATH=/headers +PROXY_HOST=localhost +PROXY_PORT=1080 +PROXY_URI="socks5h://testuser:testpass@${PROXY_HOST}:${PROXY_PORT}" +EXECUTABLE=./bin/http_client_app/http_client_app + +run_case() { + echo "" + echo "" + local test_title="$1" + echo "===== $test_title =====" + shift + "$@" + 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 +test_direct_http() { + run_case "Direct HTTP (no proxy, no TLS)" \ + $EXECUTABLE --host $HOST --port $PORT_HTTP --path $PATH +} + +test_direct_https() { + run_case "Direct HTTPS (no proxy, TLS)" \ + $EXECUTABLE --host $HOST --port $PORT_HTTPS --path $PATH +} + +test_proxy_http() { + run_case "Proxy HTTP (SOCKS5, no TLS)" \ + $EXECUTABLE --host $HOST --port $PORT_HTTP --path $PATH \ + --proxy "$PROXY_URI" +} + +test_proxy_https() { + run_case "Proxy HTTPS (SOCKS5, TLS)" \ + $EXECUTABLE --host $HOST --port $PORT_HTTPS --path $PATH \ + --proxy "$PROXY_URI" +} + +# Call all test cases + +# Local cases +HOST=$LOCAL_HOST +# Enable local http server +#test_direct_http +#test_direct_https +#test_proxy_http +#test_proxy_https + +# Remote cases +HOST=$REMOTE_HOST +test_direct_http +test_direct_https +test_proxy_http +test_proxy_https + +print_summary diff --git a/source/connection.c b/source/connection.c index e51507e00..0b42536c7 100644 --- a/source/connection.c +++ b/source/connection.c @@ -21,6 +21,7 @@ #include #include #include +#include #ifdef _MSC_VER # pragma warning(disable : 4204) /* non-constant aggregate initializer */ @@ -36,6 +37,19 @@ static const struct aws_http_connection_system_vtable *s_system_vtable_ptr = &s_ void aws_http_client_bootstrap_destroy(struct aws_http_client_bootstrap *bootstrap) { /* During allocating, the underlying stuctures should be allocated with the bootstrap by aws_mem_acquire_many. Thus, * we only need to clean up the first pointer which is the bootstrap */ + if (!bootstrap) { + return; + } + + if (aws_atomic_exchange_int(&bootstrap->destroyed, 1)) { + AWS_LOGF_TRACE( + AWS_LS_HTTP_CONNECTION, + "static: Ignoring duplicate destroy for http client bootstrap %p", + (void *)bootstrap); + return; + } + + AWS_LOGF_DEBUG(AWS_LS_HTTP_CONNECTION, "static: Destroying http client bootstrap %p", (void *)bootstrap); if (bootstrap->alpn_string_map) { aws_hash_table_clean_up(bootstrap->alpn_string_map); } @@ -811,6 +825,13 @@ static void s_client_bootstrap_on_channel_setup( (void)channel_bootstrap; AWS_ASSERT(user_data); struct aws_http_client_bootstrap *http_bootstrap = user_data; + AWS_LOGF_DEBUG( + AWS_LS_HTTP_CONNECTION, + "static: channel shutdown for http bootstrap %p (on_setup=%p on_shutdown=%p error=%d)", + (void *)http_bootstrap, + (void *)(uintptr_t)http_bootstrap->on_setup, + (void *)(uintptr_t)http_bootstrap->on_shutdown, + error_code); /* Contract for setup callbacks is: channel is NULL if error_code is non-zero. */ AWS_FATAL_ASSERT((error_code != 0) == (channel == NULL)); @@ -1101,6 +1122,7 @@ int aws_http_client_connect_internal( sizeof(struct aws_hash_table)); AWS_ZERO_STRUCT(*http_bootstrap); + aws_atomic_init_int(&http_bootstrap->destroyed, 0); http_bootstrap->alloc = options.allocator; http_bootstrap->is_using_tls = options.tls_options != NULL; @@ -1155,7 +1177,12 @@ int aws_http_client_connect_internal( .host_resolution_override_config = options.host_resolution_config, }; - err = s_system_vtable_ptr->aws_client_bootstrap_new_socket_channel(&channel_options); + if (options.socks5_proxy_options != NULL) { + err = aws_client_bootstrap_new_socket_channel_with_socks5( + options.allocator, &channel_options, options.socks5_proxy_options); + } else { + err = s_system_vtable_ptr->aws_client_bootstrap_new_socket_channel(&channel_options); + } if (err) { AWS_LOGF_ERROR( diff --git a/source/connection_manager.c b/source/connection_manager.c index aa24f7bd6..531f0d191 100644 --- a/source/connection_manager.c +++ b/source/connection_manager.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -239,6 +240,7 @@ struct aws_http_connection_manager { struct aws_string *host; struct proxy_env_var_settings proxy_ev_settings; struct aws_tls_connection_options *proxy_ev_tls_options; + struct aws_socks5_proxy_options *socks5_proxy_options; uint32_t port; uint64_t response_first_byte_timeout_ms; @@ -724,6 +726,10 @@ static void s_aws_http_connection_manager_finish_destroy(void *user_data) { if (manager->proxy_config) { aws_http_proxy_config_destroy(manager->proxy_config); } + if (manager->socks5_proxy_options) { + aws_socks5_proxy_options_clean_up(manager->socks5_proxy_options); + aws_mem_release(manager->allocator, manager->socks5_proxy_options); + } for (size_t i = 0; i < aws_array_list_length(&manager->network_interface_names); i++) { struct aws_string *interface_name = NULL; @@ -731,6 +737,10 @@ static void s_aws_http_connection_manager_finish_destroy(void *user_data) { aws_string_destroy(interface_name); } aws_array_list_clean_up(&manager->network_interface_names); + if (manager->socks5_proxy_options) { + aws_socks5_proxy_options_clean_up(manager->socks5_proxy_options); + aws_mem_release(manager->allocator, manager->socks5_proxy_options); + } /* * If this task exists then we are actually in the corresponding event loop running the final destruction task. @@ -943,6 +953,16 @@ struct aws_http_connection_manager *aws_http_connection_manager_new( } } + if (options->socks5_proxy_options) { + manager->socks5_proxy_options = aws_mem_calloc(allocator, 1, sizeof(struct aws_socks5_proxy_options)); + if (manager->socks5_proxy_options == NULL) { + goto on_error; + } + if (aws_socks5_proxy_options_copy(manager->socks5_proxy_options, options->socks5_proxy_options)) { + goto on_error; + } + } + if (options->monitoring_options) { manager->monitoring_options = *options->monitoring_options; } @@ -963,7 +983,7 @@ struct aws_http_connection_manager *aws_http_connection_manager_new( manager->max_pending_connection_acquisitions = options->max_pending_connection_acquisitions; manager->response_first_byte_timeout_ms = options->response_first_byte_timeout_ms; - if (options->proxy_ev_settings) { + if (options->proxy_ev_settings && options->socks5_proxy_options == NULL) { manager->proxy_ev_settings = *options->proxy_ev_settings; } if (manager->proxy_ev_settings.tls_options) { @@ -1118,8 +1138,11 @@ static int s_aws_http_connection_manager_new_connection(struct aws_http_connecti options.on_setup = s_aws_http_connection_manager_on_connection_setup; options.on_shutdown = s_aws_http_connection_manager_on_connection_shutdown; options.manual_window_management = manager->enable_read_back_pressure; - options.proxy_ev_settings = &manager->proxy_ev_settings; + if (manager->socks5_proxy_options == NULL) { + options.proxy_ev_settings = &manager->proxy_ev_settings; + } options.prior_knowledge_http2 = manager->http2_prior_knowledge; + options.socks5_proxy_options = manager->socks5_proxy_options; struct aws_http2_connection_options h2_options; AWS_ZERO_STRUCT(h2_options); @@ -1143,7 +1166,7 @@ static int s_aws_http_connection_manager_new_connection(struct aws_http_connecti struct aws_http_proxy_options proxy_options; AWS_ZERO_STRUCT(proxy_options); - if (manager->proxy_config) { + if (manager->proxy_config && manager->socks5_proxy_options == NULL) { aws_http_proxy_options_init_from_config(&proxy_options, manager->proxy_config); options.proxy_options = &proxy_options; } diff --git a/source/websocket_bootstrap.c b/source/websocket_bootstrap.c index 6c66c8515..ce98cf12a 100644 --- a/source/websocket_bootstrap.c +++ b/source/websocket_bootstrap.c @@ -86,6 +86,9 @@ struct aws_websocket_client_bootstrap { int setup_error_code; struct aws_websocket *websocket; + + /* prevent double-destruction when multiple teardown paths race */ + bool cleanup_invoked; }; static void s_ws_bootstrap_destroy(struct aws_websocket_client_bootstrap *ws_bootstrap); @@ -147,6 +150,13 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); } + if (options->proxy_options != NULL && options->socks5_proxy_options != NULL) { + AWS_LOGF_ERROR( + AWS_LS_HTTP_WEBSOCKET_SETUP, + "id=static: SOCKS5 proxy options and HTTP proxy options are mutually exclusive."); + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + const struct aws_http_headers *request_headers = aws_http_message_get_headers(options->handshake_request); struct aws_byte_cursor sec_websocket_key; if (aws_http_headers_get(request_headers, aws_byte_cursor_from_c_str("Sec-WebSocket-Key"), &sec_websocket_key)) { @@ -197,6 +207,7 @@ int aws_websocket_client_connect(const struct aws_websocket_client_connection_op http_options.socket_options = options->socket_options; http_options.tls_options = options->tls_options; http_options.proxy_options = options->proxy_options; + http_options.socks5_proxy_options = options->socks5_proxy_options; if (options->manual_window_management) { http_options.manual_window_management = true; @@ -253,6 +264,11 @@ static void s_ws_bootstrap_destroy(struct aws_websocket_client_bootstrap *ws_boo return; } + if (ws_bootstrap->cleanup_invoked) { + return; + } + ws_bootstrap->cleanup_invoked = true; + aws_http_message_release(ws_bootstrap->handshake_request); aws_http_headers_release(ws_bootstrap->response_headers); aws_byte_buf_clean_up(&ws_bootstrap->expected_sec_websocket_accept); @@ -410,10 +426,17 @@ static void s_ws_bootstrap_invoke_setup_callback(struct aws_websocket_client_boo .handshake_response_body = response_body_ptr, }; - ws_bootstrap->websocket_setup_callback(&setup_data, ws_bootstrap->user_data); + if (ws_bootstrap->websocket_setup_callback) { + ws_bootstrap->websocket_setup_callback(&setup_data, ws_bootstrap->user_data); - /* Clear setup callback so that we know that it's been invoked. */ - ws_bootstrap->websocket_setup_callback = NULL; + /* Clear setup callback so that we know that it's been invoked. */ + ws_bootstrap->websocket_setup_callback = NULL; + } else { + AWS_LOGF_WARN( + AWS_LS_HTTP_WEBSOCKET_SETUP, + "id=%p: Websocket setup callback already cleared; skipping invocation.", + (void *)ws_bootstrap); + } if (response_header_array) { aws_mem_release(ws_bootstrap->alloc, response_header_array); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 836df7a0d..b11745304 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -558,6 +558,7 @@ add_net_test_case(connection_manager_close_and_release) add_net_test_case(connection_manager_acquire_release_mix) add_net_test_case(connection_manager_max_pending_acquisitions) add_net_test_case(connection_manager_max_pending_acquisitions_with_vended_connections) +add_net_test_case(test_connection_manager_socks5_proxy_connection) # Integration test that requires proxy envrionment in us-east-1 region. # TODO: test the server name validation properly @@ -624,6 +625,7 @@ add_test_case(h1_server_close_from_on_thread_makes_not_open) add_test_case(http_forwarding_proxy_connection_proxy_target) add_test_case(http_forwarding_proxy_connection_channel_failure) add_test_case(http_forwarding_proxy_connection_connect_failure) +add_test_case(test_http_socks5_proxy_connection_channel_failure) add_test_case(http_forwarding_proxy_request_transform) add_test_case(http_forwarding_proxy_request_transform_basic_auth) add_test_case(http_forwarding_proxy_request_transform_legacy_basic_auth) @@ -657,6 +659,8 @@ add_test_case(http_tunnel_proxy_connection_failure_connect) add_test_case(https_tunnel_proxy_connection_failure_connect) add_test_case(https_tunnel_proxy_connection_failure_tls) +add_test_case(test_http_socks5_proxy_connection_channel_failure) + add_test_case(http_connection_monitor_options_is_valid) add_test_case(http_connection_monitor_rw_above) add_test_case(http_connection_monitor_r_above) diff --git a/tests/proxy_test_helper.c b/tests/proxy_test_helper.c index a72ab1bc9..7c7fd4162 100644 --- a/tests/proxy_test_helper.c +++ b/tests/proxy_test_helper.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -120,7 +121,13 @@ int proxy_tester_init(struct proxy_tester *tester, const struct proxy_tester_opt tester->host = options->host; tester->port = options->port; - tester->proxy_options = *options->proxy_options; + if (options->proxy_options) { + tester->proxy_options = *options->proxy_options; + } else { + AWS_ZERO_STRUCT(tester->proxy_options); + } + tester->socks5_proxy_options = options->socks5_proxy_options; + tester->socks5_invocations = 0; tester->test_mode = options->test_mode; tester->failure_type = options->failure_type; @@ -194,6 +201,9 @@ int proxy_tester_init(struct proxy_tester *tester, const struct proxy_tester_opt if (options->proxy_options) { client_options.proxy_options = options->proxy_options; } + if (options->socks5_proxy_options) { + client_options.socks5_proxy_options = options->socks5_proxy_options; + } aws_http_client_connect(&client_options); @@ -275,6 +285,8 @@ int proxy_tester_clean_up(struct proxy_tester *tester) { aws_byte_buf_clean_up(&tester->connection_host_name); + aws_socks5_channel_handler_set_system_vtable(NULL); + return AWS_OP_SUCCESS; } diff --git a/tests/proxy_test_helper.h b/tests/proxy_test_helper.h index 796efba9a..ac0b5c1a7 100644 --- a/tests/proxy_test_helper.h +++ b/tests/proxy_test_helper.h @@ -13,6 +13,7 @@ #include #include #include +#include struct aws_http_client_bootstrap; struct testing_channel; @@ -38,6 +39,7 @@ enum proxy_tester_failure_type { struct proxy_tester_options { struct aws_allocator *alloc; struct aws_http_proxy_options *proxy_options; + const struct aws_socks5_proxy_options *socks5_proxy_options; struct aws_byte_cursor host; uint32_t port; enum proxy_tester_test_mode test_mode; @@ -86,6 +88,9 @@ struct proxy_tester { struct aws_array_list connect_requests; + const struct aws_socks5_proxy_options *socks5_proxy_options; + size_t socks5_invocations; + uint32_t current_response_index; struct aws_array_list desired_connect_responses; }; diff --git a/tests/test_connection_manager.c b/tests/test_connection_manager.c index 9fe82ae45..0f00ac53f 100644 --- a/tests/test_connection_manager.c +++ b/tests/test_connection_manager.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #ifdef _MSC_VER @@ -33,6 +34,9 @@ AWS_STATIC_STRING_FROM_LITERAL(s_http_proxy_env_var_low, "http_proxy"); AWS_STATIC_STRING_FROM_LITERAL(s_https_proxy_env_var, "HTTPS_PROXY"); AWS_STATIC_STRING_FROM_LITERAL(s_https_proxy_env_var_low, "https_proxy"); +static const char *s_cm_socks5_proxy_host_name = "socks5.cm.proxy"; +static const uint16_t s_cm_socks5_proxy_port = 1080; + enum new_connection_result_type { AWS_NCRT_SUCCESS, AWS_NCRT_ERROR_VIA_CALLBACK, @@ -48,6 +52,7 @@ struct cm_tester_options { struct aws_allocator *allocator; struct aws_http_connection_manager_system_vtable *mock_table; struct aws_http_proxy_options *proxy_options; + const struct aws_socks5_proxy_options *socks5_proxy_options; bool use_proxy_env; bool use_tls; struct aws_tls_connection_options *env_configured_tls; @@ -80,6 +85,7 @@ struct cm_tester { struct aws_http_proxy_options *verify_proxy_options; const struct aws_byte_cursor *verify_network_interface_names_array; size_t num_network_interface_names; + const struct aws_socks5_proxy_options *verify_socks5_options; struct aws_mutex lock; struct aws_condition_variable signal; @@ -105,6 +111,7 @@ struct cm_tester { struct proxy_env_var_settings proxy_ev_settings; bool proxy_request_complete; bool proxy_request_successful; + size_t socks5_invocations; bool self_lib_init; }; @@ -202,6 +209,8 @@ static int s_cm_tester_init(struct cm_tester_options *options) { aws_tls_connection_options_set_server_name(&tester->tls_connection_options, options->allocator, &server_name); tester->verify_proxy_options = options->proxy_options; + tester->verify_socks5_options = options->socks5_proxy_options; + tester->socks5_invocations = 0; tester->proxy_ev_settings.env_var_type = options->use_proxy_env ? AWS_HPEV_ENABLE : AWS_HPEV_DISABLE; struct aws_tls_connection_options default_tls_connection_options; AWS_ZERO_STRUCT(default_tls_connection_options); @@ -216,6 +225,7 @@ static int s_cm_tester_init(struct cm_tester_options *options) { .socket_options = &socket_options, .tls_connection_options = options->use_tls ? &tester->tls_connection_options : NULL, .proxy_options = options->proxy_options, + .socks5_proxy_options = options->socks5_proxy_options, .proxy_ev_settings = &tester->proxy_ev_settings, .host = server_name, .port = options->use_tls ? 443 : 80, @@ -844,9 +854,22 @@ static int s_aws_http_connection_manager_create_connection_validate( ASSERT_TRUE(options->proxy_options->port == tester->verify_proxy_options->port); ASSERT_UINT_EQUALS(options->proxy_options->connection_type, tester->verify_proxy_options->connection_type); } + /* Verify that any SOCKS5 proxy options have been propagated to the connection attempt */ + if (tester->verify_socks5_options) { + ASSERT_NOT_NULL(options->socks5_proxy_options); + struct aws_byte_cursor expected_host = aws_byte_cursor_from_string(tester->verify_socks5_options->host); + struct aws_byte_cursor actual_host = aws_byte_cursor_from_string(options->socks5_proxy_options->host); + ASSERT_BIN_ARRAYS_EQUALS( + expected_host.ptr, expected_host.len, actual_host.ptr, actual_host.len, "Socks5 proxy host mismatch"); + ASSERT_UINT_EQUALS(tester->verify_socks5_options->port, options->socks5_proxy_options->port); + tester->socks5_invocations++; + } else { + ASSERT_NULL(options->socks5_proxy_options); + } return AWS_OP_SUCCESS; } + static int s_aws_http_connection_manager_create_connection_sync_mock( const struct aws_http_client_connection_options *options) { s_aws_http_connection_manager_create_connection_validate(options); @@ -1216,6 +1239,37 @@ static int s_test_connection_manager_connect_immediate_failure(struct aws_alloca } AWS_TEST_CASE(connection_manager_connect_immediate_failure, s_test_connection_manager_connect_immediate_failure); +static int s_test_connection_manager_socks5_proxy_connection(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_socks5_proxy_options socks5_options; + ASSERT_SUCCESS(aws_socks5_proxy_options_init( + &socks5_options, allocator, aws_byte_cursor_from_c_str(s_cm_socks5_proxy_host_name), s_cm_socks5_proxy_port)); + aws_socks5_proxy_options_set_host_resolution_mode(&socks5_options, AWS_SOCKS5_HOST_RESOLUTION_PROXY); + + struct cm_tester_options options = { + .allocator = allocator, + .max_connections = 1, + .mock_table = &s_synchronous_mocks, + .socks5_proxy_options = &socks5_options, + }; + + ASSERT_SUCCESS(s_cm_tester_init(&options)); + + s_add_mock_connections(1, AWS_NCRT_SUCCESS, false); + s_acquire_connections(1); + + ASSERT_SUCCESS(s_wait_on_connection_reply_count(1)); + ASSERT_UINT_EQUALS(1, s_tester.socks5_invocations); + + ASSERT_SUCCESS(s_release_connections(1, false)); + ASSERT_SUCCESS(s_cm_tester_clean_up()); + aws_socks5_proxy_options_clean_up(&socks5_options); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE(test_connection_manager_socks5_proxy_connection, s_test_connection_manager_socks5_proxy_connection); + static int s_test_connection_manager_proxy_setup_shutdown(struct aws_allocator *allocator, void *ctx) { (void)ctx; diff --git a/tests/test_proxy.c b/tests/test_proxy.c index 4087e2f3e..581b5c193 100644 --- a/tests/test_proxy.c +++ b/tests/test_proxy.c @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include @@ -25,6 +27,8 @@ static char *s_host_name = "aws.amazon.com"; static uint32_t s_port = 80; static char *s_proxy_host_name = "www.myproxy.hmm"; static uint32_t s_proxy_port = 777; +static char *s_socks5_proxy_host_name = "socks5.proxy.tester"; +static uint32_t s_socks5_proxy_port = 1080; AWS_STATIC_STRING_FROM_LITERAL(s_mock_request_method, "GET"); AWS_STATIC_STRING_FROM_LITERAL(s_mock_request_path, "/"); @@ -34,6 +38,17 @@ AWS_STATIC_STRING_FROM_LITERAL(s_expected_basic_auth_header_value, "Basic U29tZV AWS_STATIC_STRING_FROM_LITERAL(s_mock_request_username, "SomeUser"); AWS_STATIC_STRING_FROM_LITERAL(s_mock_request_password, "SuperSecret"); +static int s_test_aws_proxy_new_socket_channel(struct aws_socket_channel_bootstrap_options *channel_options); + +static int s_test_aws_socks5_new_socket_channel(struct aws_socket_channel_bootstrap_options *channel_options) { + tester.socks5_invocations++; + return s_test_aws_proxy_new_socket_channel(channel_options); +} + +static struct aws_socks5_system_vtable s_proxy_socks5_vtable = { + .aws_client_bootstrap_new_socket_channel = s_test_aws_socks5_new_socket_channel, +}; + /* * Request utility functions */ @@ -155,6 +170,7 @@ static int s_test_aws_proxy_new_socket_channel(struct aws_socket_channel_bootstr * Record where we were trying to connect to */ struct aws_byte_cursor host_cursor = aws_byte_cursor_from_c_str(channel_options->host_name); + aws_byte_buf_reset(&tester.connection_host_name, false); aws_byte_buf_append_dynamic(&tester.connection_host_name, &host_cursor); tester.connection_port = channel_options->port; @@ -224,6 +240,9 @@ struct mocked_proxy_test_options { struct aws_byte_cursor legacy_basic_username; struct aws_byte_cursor legacy_basic_password; + const struct aws_socks5_proxy_options *socks5_proxy_options; + bool use_socks5_proxy; + uint32_t mocked_response_count; struct aws_byte_cursor *mocked_responses; }; @@ -235,26 +254,43 @@ static int s_setup_proxy_test(struct aws_allocator *allocator, struct mocked_pro aws_http_connection_set_system_vtable(&s_proxy_connection_system_vtable); aws_http_proxy_system_set_vtable(&s_proxy_table_for_tls); + if (tester.connection_host_name.buffer) { + aws_byte_buf_reset(&tester.connection_host_name, false); + } else { + tester.connection_host_name.len = 0; + tester.connection_host_name.capacity = 0; + } + tester.socks5_invocations = 0; + if (config->socks5_proxy_options) { + aws_socks5_channel_handler_set_system_vtable(&s_proxy_socks5_vtable); + } else { + aws_socks5_channel_handler_set_system_vtable(NULL); + } - struct aws_http_proxy_options proxy_options = { - .connection_type = (config->test_mode == PTTM_HTTP_FORWARD) ? AWS_HPCT_HTTP_FORWARD : AWS_HPCT_HTTP_TUNNEL, - .host = aws_byte_cursor_from_c_str(s_proxy_host_name), - .port = s_proxy_port, - .proxy_strategy = config->proxy_strategy, - .auth_type = config->auth_type, - .auth_username = config->legacy_basic_username, - .auth_password = config->legacy_basic_password, - }; + struct aws_http_proxy_options proxy_options; + AWS_ZERO_STRUCT(proxy_options); + bool use_http_proxy = !config->use_socks5_proxy; + if (use_http_proxy) { + proxy_options.connection_type = + (config->test_mode == PTTM_HTTP_FORWARD) ? AWS_HPCT_HTTP_FORWARD : AWS_HPCT_HTTP_TUNNEL; + proxy_options.host = aws_byte_cursor_from_c_str(s_proxy_host_name); + proxy_options.port = s_proxy_port; + proxy_options.proxy_strategy = config->proxy_strategy; + proxy_options.auth_type = config->auth_type; + proxy_options.auth_username = config->legacy_basic_username; + proxy_options.auth_password = config->legacy_basic_password; + } struct proxy_tester_options options = { .alloc = allocator, - .proxy_options = &proxy_options, + .proxy_options = use_http_proxy ? &proxy_options : NULL, .host = aws_byte_cursor_from_c_str(s_host_name), .port = s_port, .test_mode = config->test_mode, .failure_type = config->failure_type, .desired_connect_response_count = config->mocked_response_count, .desired_connect_responses = config->mocked_responses, + .socks5_proxy_options = config->socks5_proxy_options, }; ASSERT_SUCCESS(proxy_tester_init(&tester, &options)); @@ -341,6 +377,39 @@ AWS_TEST_CASE( http_forwarding_proxy_connection_connect_failure, s_test_http_forwarding_proxy_connection_connect_failure); +static int s_test_http_socks5_proxy_connection_channel_failure(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_socks5_proxy_options socks5_options; + ASSERT_SUCCESS(aws_socks5_proxy_options_init( + &socks5_options, + allocator, + aws_byte_cursor_from_c_str(s_socks5_proxy_host_name), + s_socks5_proxy_port)); + aws_socks5_proxy_options_set_host_resolution_mode(&socks5_options, AWS_SOCKS5_HOST_RESOLUTION_PROXY); + + struct mocked_proxy_test_options options = { + .test_mode = PTTM_HTTP_FORWARD, + .failure_type = PTFT_CHANNEL, + .socks5_proxy_options = &socks5_options, + .use_socks5_proxy = true, + }; + + ASSERT_SUCCESS(s_setup_proxy_test(allocator, &options)); + + ASSERT_UINT_EQUALS(1, tester.socks5_invocations); + ASSERT_TRUE(tester.wait_result != AWS_ERROR_SUCCESS); + ASSERT_TRUE(tester.client_connection == NULL); + + ASSERT_SUCCESS(proxy_tester_clean_up(&tester)); + aws_socks5_proxy_options_clean_up(&socks5_options); + + return AWS_OP_SUCCESS; +} +AWS_TEST_CASE( + test_http_socks5_proxy_connection_channel_failure, + s_test_http_socks5_proxy_connection_channel_failure); + /* * For tls-enabled tunneling proxy connections: * Test the happy path by verifying CONNECT request, tls upgrade attempt