/*
 * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
#include <aws/http/connection.h>
#include <aws/http/request_response.h>

#include <aws/common/command_line_parser.h>
#include <aws/common/condition_variable.h>
#include <aws/common/hash_table.h>
#include <aws/common/log_channel.h>
#include <aws/common/log_formatter.h>
#include <aws/common/log_writer.h>
#include <aws/common/mutex.h>
#include <aws/common/string.h>

#include <aws/io/channel_bootstrap.h>
#include <aws/io/event_loop.h>
#include <aws/io/logging.h>
#include <aws/io/shared_library.h>
#include <aws/io/socket.h>
#include <aws/io/stream.h>
#include <aws/io/tls_channel_handler.h>
#include <aws/io/uri.h>

#include <inttypes.h>

#ifdef _MSC_VER
#    pragma warning(disable : 4996) /* Disable warnings about fopen() being insecure */
#    pragma warning(disable : 4204) /* Declared initializers */
#    pragma warning(disable : 4221) /* Local var in declared initializer */
#endif

#define ELASTICURL_VERSION "0.2.0"

struct elasticurl_ctx {
    struct aws_allocator *allocator;
    const char *verb;
    struct aws_uri uri;
    struct aws_mutex mutex;
    struct aws_condition_variable c_var;
    bool response_code_written;
    const char *cacert;
    const char *capath;
    const char *cert;
    const char *key;
    int connect_timeout;
    const char *header_lines[10];
    size_t header_line_count;
    FILE *input_file;
    struct aws_input_stream *input_body;
    struct aws_http_message *request;
    struct aws_http_connection *connection;
    const char *signing_library_path;
    struct aws_shared_library signing_library;
    const char *signing_function_name;
    struct aws_hash_table signing_context;
    aws_http_message_transform_fn *signing_function;
    bool include_headers;
    bool insecure;
    FILE *output;
    const char *trace_file;
    enum aws_log_level log_level;
    bool exchange_completed;
};

static void s_usage(int exit_code) {

    fprintf(stderr, "usage: elasticurl [options] url\n");
    fprintf(stderr, " url: url to make a request to. The default is a GET request.\n");
    fprintf(stderr, "\n Options:\n\n");
    fprintf(stderr, "      --cacert FILE: path to a CA certficate file.\n");
    fprintf(stderr, "      --capath PATH: path to a directory containing CA files.\n");
    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, "  -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");
    fprintf(stderr, "  -M, --method STRING: Http Method verb to use for the request\n");
    fprintf(stderr, "  -G, --get: uses GET for the verb.\n");
    fprintf(stderr, "  -P, --post: uses POST for the verb.\n");
    fprintf(stderr, "  -I, --head: uses HEAD for the verb.\n");
    fprintf(stderr, "  -i, --include: includes headers in output.\n");
    fprintf(stderr, "  -k, --insecure: turns off SSL/TLS validation.\n");
    fprintf(stderr, "      --signing-lib: path to a shared library with an exported signing function to use\n");
    fprintf(stderr, "      --signing-func: name of the signing function to use within the signing library\n");
    fprintf(
        stderr,
        "      --signing-context: key=value pair to pass to the signing function; may be used multiple times\n");
    fprintf(stderr, "  -o, --output FILE: dumps content-body to FILE instead of stdout.\n");
    fprintf(stderr, "  -t, --trace 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, "      --version: print the version of elasticurl.\n");
    fprintf(stderr, "  -h, --help\n");
    fprintf(stderr, "            Display this message and quit.\n");
    exit(exit_code);
}

static struct aws_cli_option s_long_options[] = {
    {"cacert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'a'},
    {"capath", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'b'},
    {"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'},
    {"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'},
    {"method", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'M'},
    {"get", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'G'},
    {"post", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'P'},
    {"head", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'I'},
    {"signing-lib", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'j'},
    {"include", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'i'},
    {"insecure", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'k'},
    {"signing-func", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'l'},
    {"signing-context", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'm'},
    {"output", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'o'},
    {"trace", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 't'},
    {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'},
    {"version", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'V'},
    {"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},
};

static int s_parse_signing_context(
    struct aws_hash_table *signing_context,
    struct aws_allocator *allocator,
    const char *context_argument) {
    (void)signing_context;
    (void)context_argument;

    char *delimiter = memchr(context_argument, ':', strlen(context_argument));
    if (!delimiter) {
        fprintf(stderr, "invalid signing context line \"%s\".", context_argument);
        exit(1);
    }

    struct aws_string *key =
        aws_string_new_from_array(allocator, (const uint8_t *)context_argument, delimiter - context_argument);
    struct aws_string *value =
        aws_string_new_from_array(allocator, (const uint8_t *)delimiter + 1, strlen(delimiter + 1));
    if (key == NULL || value == NULL) {
        fprintf(stderr, "failure allocating signing context kv pair");
        exit(1);
    }

    aws_hash_table_put(signing_context, key, value, NULL);

    return AWS_OP_SUCCESS;
}

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:Vh", s_long_options, &option_index);
        if (c == -1) {
            break;
        }

        switch (c) {
            case 0:
                /* getopt_long() returns 0 if an option.flag is non-null */
                break;
            case 'a':
                ctx->cacert = aws_cli_optarg;
                break;
            case 'b':
                ctx->capath = aws_cli_optarg;
                break;
            case 'c':
                ctx->cert = aws_cli_optarg;
                break;
            case 'e':
                ctx->key = aws_cli_optarg;
                break;
            case 'f':
                ctx->connect_timeout = atoi(aws_cli_optarg);
                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");
                    s_usage(1);
                }
                ctx->header_lines[ctx->header_line_count++] = aws_cli_optarg;
                break;
            case 'd': {
                struct aws_byte_cursor data_cursor = aws_byte_cursor_from_c_str(aws_cli_optarg);
                ctx->input_body = aws_input_stream_new_from_cursor(ctx->allocator, &data_cursor);
                break;
            }
            case 'g':
                ctx->input_file = fopen(aws_cli_optarg, "rb");
                ctx->input_body = aws_input_stream_new_from_open_file(ctx->allocator, ctx->input_file);
                if (!ctx->input_file) {
                    fprintf(stderr, "unable to open file %s.\n", aws_cli_optarg);
                    s_usage(1);
                }
                break;
            case 'j':
                ctx->signing_library_path = aws_cli_optarg;
                if (aws_shared_library_init(&ctx->signing_library, aws_cli_optarg)) {
                    fprintf(stderr, "unable to open signing library %s.\n", aws_cli_optarg);
                    s_usage(1);
                }
                break;
            case 'l':
                ctx->signing_function_name = aws_cli_optarg;
                break;
            case 'm':
                if (s_parse_signing_context(&ctx->signing_context, ctx->allocator, aws_cli_optarg)) {
                    fprintf(stderr, "error parsing signing context \"%s\"\n", aws_cli_optarg);
                    s_usage(1);
                }
                break;
            case 'M':
                ctx->verb = aws_cli_optarg;
                break;
            case 'G':
                ctx->verb = "GET";
                break;
            case 'P':
                ctx->verb = "POST";
                break;
            case 'I':
                ctx->verb = "HEAD";
                break;
            case 'i':
                ctx->include_headers = true;
                break;
            case 'k':
                ctx->insecure = true;
                break;
            case 'o':
                ctx->output = fopen(aws_cli_optarg, "wb");

                if (!ctx->output) {
                    fprintf(stderr, "unable to open file %s.\n", aws_cli_optarg);
                    s_usage(1);
                }
                break;
            case 't':
                ctx->trace_file = aws_cli_optarg;
                break;
            case 'v':
                if (!strcmp(aws_cli_optarg, "TRACE")) {
                    ctx->log_level = AWS_LL_TRACE;
                } else if (!strcmp(aws_cli_optarg, "INFO")) {
                    ctx->log_level = AWS_LL_INFO;
                } else if (!strcmp(aws_cli_optarg, "DEBUG")) {
                    ctx->log_level = AWS_LL_DEBUG;
                } else if (!strcmp(aws_cli_optarg, "ERROR")) {
                    ctx->log_level = AWS_LL_ERROR;
                } else {
                    fprintf(stderr, "unsupported log level %s.\n", aws_cli_optarg);
                    s_usage(1);
                }
                break;
            case 'V':
                fprintf(stderr, "elasticurl %s\n", ELASTICURL_VERSION);
                exit(0);
            case 'h':
                s_usage(0);
                break;
            default:
                fprintf(stderr, "Unknown option\n");
                s_usage(1);
        }
    }

    if (ctx->signing_function_name != NULL) {
        if (ctx->signing_library_path == NULL) {
            fprintf(
                stderr,
                "To sign a request made by Elasticurl you must supply both a signing library path and a signing "
                "function name\n");
            s_usage(1);
        }

        if (aws_shared_library_find_function(
                &ctx->signing_library, ctx->signing_function_name, (aws_generic_function *)&ctx->signing_function)) {
            fprintf(
                stderr,
                "Unable to find function %s in signing library %s",
                ctx->signing_function_name,
                ctx->signing_library_path);
            s_usage(1);
        }
    }

    if (ctx->input_body == NULL) {
        struct aws_byte_cursor empty_cursor;
        AWS_ZERO_STRUCT(empty_cursor);
        ctx->input_body = aws_input_stream_new_from_cursor(ctx->allocator, &empty_cursor);
    }

    if (aws_cli_optind < argc) {
        struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(argv[aws_cli_optind++]);

        if (aws_uri_init_parse(&ctx->uri, ctx->allocator, &uri_cursor)) {
            fprintf(
                stderr,
                "Failed to parse uri %s with error %s\n",
                (char *)uri_cursor.ptr,
                aws_error_debug_str(aws_last_error()));
            s_usage(1);
        };
    } else {
        fprintf(stderr, "A URI for the request must be supplied.\n");
        s_usage(1);
    }
}

static int s_on_incoming_body_fn(struct aws_http_stream *stream, const struct aws_byte_cursor *data, void *user_data) {

    (void)stream;
    struct elasticurl_ctx *app_ctx = user_data;

    fwrite(data->ptr, 1, data->len, app_ctx->output);

    return AWS_OP_SUCCESS;
}

static int s_on_incoming_headers_fn(
    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 elasticurl_ctx *app_ctx = user_data;
    (void)app_ctx;
    (void)stream;

    /* Ignore informational headers */
    if (header_block == AWS_HTTP_HEADER_BLOCK_INFORMATIONAL) {
        return AWS_OP_SUCCESS;
    }

    if (app_ctx->include_headers) {
        if (!app_ctx->response_code_written) {
            int status = 0;
            aws_http_stream_get_incoming_response_status(stream, &status);
            fprintf(stdout, "Response Status: %d\n", status);
            app_ctx->response_code_written = true;
        }

        for (size_t i = 0; i < num_headers; ++i) {
            fwrite(header_array[i].name.ptr, 1, header_array[i].name.len, stdout);
            fprintf(stdout, ":");
            fwrite(header_array[i].value.ptr, 1, header_array[i].value.len, stdout);
            fprintf(stdout, "\n");
        }
    }

    return AWS_OP_SUCCESS;
}

static int s_on_incoming_header_block_done_fn(
    struct aws_http_stream *stream,
    enum aws_http_header_block header_block,
    void *user_data) {
    (void)stream;
    (void)header_block;
    (void)user_data;

    return AWS_OP_SUCCESS;
}

static void s_on_stream_complete_fn(struct aws_http_stream *stream, int error_code, void *user_data) {
    (void)error_code;
    (void)user_data;
    aws_http_stream_release(stream);
}

static struct aws_http_message *s_build_http_request(struct elasticurl_ctx *app_ctx) {
    struct aws_http_message *request = aws_http_message_new_request(app_ctx->allocator);
    if (request == NULL) {
        fprintf(stderr, "failed to allocate request\n");
        exit(1);
    }

    aws_http_message_set_request_method(request, aws_byte_cursor_from_c_str(app_ctx->verb));
    aws_http_message_set_request_path(request, app_ctx->uri.path_and_query);
    struct aws_http_header accept_header = {.name = aws_byte_cursor_from_c_str("accept"),
                                            .value = aws_byte_cursor_from_c_str("*/*")};
    aws_http_message_add_header(request, accept_header);

    struct aws_http_header host_header = {.name = aws_byte_cursor_from_c_str("host"), .value = app_ctx->uri.host_name};
    aws_http_message_add_header(request, host_header);

    struct aws_http_header user_agent_header = {
        .name = aws_byte_cursor_from_c_str("user-agent"),
        .value = aws_byte_cursor_from_c_str("elasticurl 1.0, Powered by the AWS Common Runtime.")};
    aws_http_message_add_header(request, user_agent_header);

    if (app_ctx->input_body) {
        int64_t data_len = 0;
        if (aws_input_stream_get_length(app_ctx->input_body, &data_len)) {
            fprintf(stderr, "failed to get length of input stream.\n");
            exit(1);
        }

        if (data_len > 0) {
            char content_length[64];
            AWS_ZERO_ARRAY(content_length);
            sprintf(content_length, "%" PRIi64, data_len);
            struct aws_http_header content_length_header = {.name = aws_byte_cursor_from_c_str("content-length"),
                                                            .value = aws_byte_cursor_from_c_str(content_length)};
            aws_http_message_add_header(request, content_length_header);
            aws_http_message_set_body_stream(request, app_ctx->input_body);
        }
    }

    AWS_ASSERT(app_ctx->header_line_count <= 10);
    for (size_t i = 0; i < app_ctx->header_line_count; ++i) {
        char *delimiter = memchr(app_ctx->header_lines[i], ':', strlen(app_ctx->header_lines[i]));

        if (!delimiter) {
            fprintf(stderr, "invalid header line %s configured.", app_ctx->header_lines[i]);
            exit(1);
        }

        struct aws_http_header custom_header = {
            .name = aws_byte_cursor_from_array(app_ctx->header_lines[i], delimiter - app_ctx->header_lines[i]),
            .value = aws_byte_cursor_from_c_str(delimiter + 1)};
        aws_http_message_add_header(request, custom_header);
    }

    return request;
}

static void s_on_signing_complete(struct aws_http_message *request, int error_code, void *user_data);

static void s_on_client_connection_setup(struct aws_http_connection *connection, int error_code, void *user_data) {
    struct elasticurl_ctx *app_ctx = user_data;

    if (error_code) {
        fprintf(stderr, "Connection failed with error %s\n", aws_error_debug_str(error_code));
        aws_mutex_lock(&app_ctx->mutex);
        app_ctx->exchange_completed = true;
        aws_mutex_unlock(&app_ctx->mutex);
        aws_condition_variable_notify_all(&app_ctx->c_var);
        return;
    }

    app_ctx->connection = connection;
    app_ctx->request = s_build_http_request(app_ctx);

    /* If async signing function is set, invoke it. It must invoke the signing complete callback when it's done. */
    if (app_ctx->signing_function) {
        app_ctx->signing_function(app_ctx->request, &app_ctx->signing_context, s_on_signing_complete, app_ctx);
    } else {
        /* If no signing function, proceed immediately to next step. */
        s_on_signing_complete(app_ctx->request, AWS_ERROR_SUCCESS, app_ctx);
    }
}

static void s_on_signing_complete(struct aws_http_message *request, int error_code, void *user_data) {
    struct elasticurl_ctx *app_ctx = user_data;

    AWS_FATAL_ASSERT(request == app_ctx->request);

    if (error_code) {
        fprintf(stderr, "Signing failure\n");
        exit(1);
    }

    size_t final_header_count = aws_http_message_get_header_count(app_ctx->request);

    struct aws_http_header headers[20];
    AWS_ASSERT(final_header_count <= AWS_ARRAY_SIZE(headers));
    AWS_ZERO_ARRAY(headers);
    for (size_t i = 0; i < final_header_count; ++i) {
        aws_http_message_get_header(app_ctx->request, &headers[i], i);
    }

    struct aws_http_make_request_options final_request = {
        .self_size = sizeof(final_request),
        .user_data = app_ctx,
        .request = app_ctx->request,
        .on_response_headers = s_on_incoming_headers_fn,
        .on_response_header_block_done = s_on_incoming_header_block_done_fn,
        .on_response_body = s_on_incoming_body_fn,
        .on_complete = s_on_stream_complete_fn,
    };

    app_ctx->response_code_written = false;

    struct aws_http_stream *stream = aws_http_connection_make_request(app_ctx->connection, &final_request);
    if (!stream) {
        fprintf(stderr, "failed to create request.");
        exit(1);
    }

    /* Connection will stay alive until stream completes */
    aws_http_connection_release(app_ctx->connection);
    app_ctx->connection = NULL;
}

static void s_on_client_connection_shutdown(struct aws_http_connection *connection, int error_code, void *user_data) {
    (void)error_code;
    (void)connection;
    struct elasticurl_ctx *app_ctx = user_data;

    aws_mutex_lock(&app_ctx->mutex);
    app_ctx->exchange_completed = true;
    aws_mutex_unlock(&app_ctx->mutex);
    aws_condition_variable_notify_all(&app_ctx->c_var);
}

static bool s_completion_predicate(void *arg) {
    struct elasticurl_ctx *app_ctx = arg;
    return app_ctx->exchange_completed;
}

int main(int argc, char **argv) {
    struct aws_allocator *allocator = aws_default_allocator();

    aws_http_library_init(allocator);

    struct elasticurl_ctx app_ctx;
    AWS_ZERO_STRUCT(app_ctx);
    app_ctx.allocator = allocator;
    app_ctx.c_var = (struct aws_condition_variable)AWS_CONDITION_VARIABLE_INIT;
    app_ctx.connect_timeout = 3000;
    app_ctx.output = stdout;
    app_ctx.verb = "GET";
    aws_mutex_init(&app_ctx.mutex);
    aws_hash_table_init(
        &app_ctx.signing_context,
        allocator,
        10,
        aws_hash_string,
        aws_hash_callback_string_eq,
        aws_hash_callback_string_destroy,
        aws_hash_callback_string_destroy);

    s_parse_options(argc, argv, &app_ctx);

    struct aws_logger logger;
    AWS_ZERO_STRUCT(logger);

    if (app_ctx.log_level) {
        struct aws_logger_standard_options options = {
            .level = app_ctx.log_level,
        };

        if (app_ctx.trace_file) {
            options.filename = app_ctx.trace_file;
        } else {
            options.file = stderr;
        }

        if (aws_logger_init_standard(&logger, allocator, &options)) {
            fprintf(stderr, "Failed to initialize logger with error %s\n", aws_error_debug_str(aws_last_error()));
            exit(1);
        }

        aws_logger_set(&logger);
    }

    bool use_tls = true;
    uint16_t port = 443;

    if (!app_ctx.uri.scheme.len && (app_ctx.uri.port == 80 || app_ctx.uri.port == 8080)) {
        use_tls = false;
    } else {
        if (aws_byte_cursor_eq_c_str_ignore_case(&app_ctx.uri.scheme, "http")) {
            use_tls = false;
        }
    }

    struct aws_tls_ctx *tls_ctx = NULL;
    struct aws_tls_ctx_options tls_ctx_options;
    AWS_ZERO_STRUCT(tls_ctx_options);
    struct aws_tls_connection_options tls_connection_options;
    AWS_ZERO_STRUCT(tls_connection_options);
    struct aws_tls_connection_options *tls_options = NULL;

    if (use_tls) {
        if (app_ctx.cert && app_ctx.key) {
            if (aws_tls_ctx_options_init_client_mtls_from_path(
                    &tls_ctx_options, allocator, app_ctx.cert, app_ctx.key)) {
                fprintf(
                    stderr,
                    "Failed to load %s and %s with error %s.",
                    app_ctx.cert,
                    app_ctx.key,
                    aws_error_debug_str(aws_last_error()));
                exit(1);
            }
        }
#ifdef _WIN32
        else if (app_ctx.cert && !app_ctx.key) {
            aws_tls_ctx_options_init_client_mtls_from_system_path(&tls_ctx_options, allocator, app_ctx.cert);
        }
#endif
        else {
            aws_tls_ctx_options_init_default_client(&tls_ctx_options, allocator);
        }

        if (app_ctx.capath || app_ctx.cacert) {
            if (aws_tls_ctx_options_override_default_trust_store_from_path(
                    &tls_ctx_options, app_ctx.capath, app_ctx.cacert)) {
                fprintf(
                    stderr,
                    "Failed to load %s and %s with error %s",
                    app_ctx.capath,
                    app_ctx.cacert,
                    aws_error_debug_str(aws_last_error()));
                exit(1);
            }
        }

        if (app_ctx.insecure) {
            aws_tls_ctx_options_set_verify_peer(&tls_ctx_options, false);
        }

        /* "h2;http/1.1", add this back when we have h2 support */
        if (aws_tls_ctx_options_set_alpn_list(&tls_ctx_options, "http/1.1")) {
            fprintf(stderr, "Failed to load alpn list with error %s.", aws_error_debug_str(aws_last_error()));
            exit(1);
        }

        tls_ctx = aws_tls_client_ctx_new(allocator, &tls_ctx_options);

        if (!tls_ctx) {
            fprintf(stderr, "Failed to initialize TLS context with error %s.", aws_error_debug_str(aws_last_error()));
            exit(1);
        }

        aws_tls_connection_options_init_from_ctx(&tls_connection_options, tls_ctx);
        if (aws_tls_connection_options_set_server_name(&tls_connection_options, allocator, &app_ctx.uri.host_name)) {
            fprintf(stderr, "Failed to set servername with error %s.", aws_error_debug_str(aws_last_error()));
            exit(1);
        }
        tls_options = &tls_connection_options;

        if (app_ctx.uri.port) {
            port = app_ctx.uri.port;
        }
    } else {
        port = 80;
        if (app_ctx.uri.port) {
            port = app_ctx.uri.port;
        }
    }

    struct aws_event_loop_group el_group;
    aws_event_loop_group_default_init(&el_group, allocator, 1);

    struct aws_host_resolver resolver;
    aws_host_resolver_init_default(&resolver, allocator, 8, &el_group);

    struct aws_client_bootstrap *bootstrap = aws_client_bootstrap_new(allocator, &el_group, &resolver, NULL);

    struct aws_socket_options socket_options = {
        .type = AWS_SOCKET_STREAM,
        .connect_timeout_ms = (uint32_t)app_ctx.connect_timeout,
        .keep_alive_timeout_sec = 0,
        .keepalive = false,
        .keep_alive_interval_sec = 0,
    };

    struct aws_http_client_connection_options http_client_options = {
        .self_size = sizeof(struct aws_http_client_connection_options),
        .socket_options = &socket_options,
        .allocator = allocator,
        .port = port,
        .host_name = app_ctx.uri.host_name,
        .bootstrap = bootstrap,
        .initial_window_size = SIZE_MAX,
        .tls_options = tls_options,
        .user_data = &app_ctx,
        .on_setup = s_on_client_connection_setup,
        .on_shutdown = s_on_client_connection_shutdown,
    };

    aws_http_client_connect(&http_client_options);
    aws_mutex_lock(&app_ctx.mutex);
    aws_condition_variable_wait_pred(&app_ctx.c_var, &app_ctx.mutex, s_completion_predicate, &app_ctx);

    aws_client_bootstrap_release(bootstrap);
    aws_host_resolver_clean_up(&resolver);
    aws_event_loop_group_clean_up(&el_group);

    if (tls_ctx) {
        aws_tls_connection_options_clean_up(&tls_connection_options);
        aws_tls_ctx_destroy(tls_ctx);
        aws_tls_ctx_options_clean_up(&tls_ctx_options);
    }

    aws_http_library_clean_up();

    if (app_ctx.log_level) {
        aws_logger_clean_up(&logger);
    }

    aws_uri_clean_up(&app_ctx.uri);

    aws_http_message_destroy(app_ctx.request);

    aws_shared_library_clean_up(&app_ctx.signing_library);

    if (app_ctx.output != stdout) {
        fclose(app_ctx.output);
    }

    if (app_ctx.input_body) {
        aws_input_stream_destroy(app_ctx.input_body);
    }

    if (app_ctx.input_file) {
        fclose(app_ctx.input_file);
    }

    aws_hash_table_clean_up(&app_ctx.signing_context);

    return 0;
}
