From 02f0262dc28dd3367633fa5d645a55c15dfae913 Mon Sep 17 00:00:00 2001 From: Wesley Pettit Date: Sat, 1 Feb 2020 17:35:57 -0800 Subject: [PATCH] ECS provider --- include/fluent-bit/flb_aws_credentials.h | 37 ++ plugins/out_stdout/stdout.c | 229 ++++++++++++ src/flb_aws_credentials.c | 454 ++++++++++++++++++++++- tests/internal/CMakeLists.txt | 1 + tests/internal/aws_credentials.c | 6 +- tests/internal/aws_credentials_http.c | 357 ++++++++++++++++++ 6 files changed, 1081 insertions(+), 3 deletions(-) create mode 100644 tests/internal/aws_credentials_http.c diff --git a/include/fluent-bit/flb_aws_credentials.h b/include/fluent-bit/flb_aws_credentials.h index 539f124f1f7..203eb916570 100644 --- a/include/fluent-bit/flb_aws_credentials.h +++ b/include/fluent-bit/flb_aws_credentials.h @@ -35,6 +35,16 @@ #define AWS_SECRET_ACCESS_KEY "AWS_SECRET_ACCESS_KEY" #define AWS_SESSION_TOKEN "AWS_SESSION_TOKEN" +/* HTTP Credentials Endpoints have a standard set of JSON Keys */ +#define AWS_HTTP_RESPONSE_ACCESS_KEY "AccessKeyId" +#define AWS_HTTP_RESPONSE_SECRET_KEY "SecretAccessKey" +#define AWS_HTTP_RESPONSE_TOKEN "Token" +#define AWS_HTTP_RESPONSE_EXPIRATION "Expiration" + +#define ECS_CREDENTIALS_HOST "169.254.170.2" +#define ECS_CREDENTIALS_HOST_LEN 13 +#define ECS_CREDENTIALS_PATH_ENV_VAR "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" + /* * A structure that wraps the sensitive data needed to sign an AWS request */ @@ -143,6 +153,33 @@ struct aws_credentials_provider *new_sts_provider(struct flb_config *config, */ struct aws_credentials_provider *new_environment_provider(); + +/* + * New http provider - retrieve credentials from a local http server. + * Equivalent to: + * https://github.com/aws/aws-sdk-go/tree/master/aws/credentials/endpointcreds + * + * Calling aws_provider_destroy on this provider frees the memory + * used by host and path. + */ +struct aws_credentials_provider *new_http_provider(struct flb_config *config, + flb_sds_t host, + flb_sds_t path, + struct + aws_http_client_generator + *generator); + +/* + * ECS Provider + * The ECS Provider is just a wrapper around the HTTP Provider + * with the ECS credentials endpoint. + */ + +struct aws_credentials_provider *new_ecs_provider(struct flb_config *config, + struct + aws_http_client_generator + *generator); + /* * Helper functions */ diff --git a/plugins/out_stdout/stdout.c b/plugins/out_stdout/stdout.c index 98e3886e043..aee23a0798e 100644 --- a/plugins/out_stdout/stdout.c +++ b/plugins/out_stdout/stdout.c @@ -29,6 +29,233 @@ #include "stdout.h" +#include +#include +#include +#include +#include + +#include +#include +#include + +#define ACCESS_KEY_HTTP "http_akid" +#define SECRET_KEY_HTTP "http_skid" +#define TOKEN_HTTP "http_token" + +#define HTTP_CREDENTIALS_RESPONSE "{\n\ + \"AccessKeyId\": \"http_akid\",\n\ + \"Expiration\": \"2014-10-24T23:00:23Z\",\n\ + \"RoleArn\": \"TASK_ROLE_ARN\",\n\ + \"SecretAccessKey\": \"http_skid\",\n\ + \"Token\": \"http_token\"\n\ +}" + +/* + * Unexpected/invalid HTTP response. The goal of this is not to test anything + * that might happen in production, but rather to test the error handling + * code for the providers. This helps ensure all code paths are tested and + * the error handling code does not introduce memory leaks. + */ +#define HTTP_RESPONSE_MALFORMED "{\n\ + \"AccessKeyId\": \"http_akid\",\n\ + \"partially-correct\": \"json\",\n\ + \"RoleArn\": \"TASK_ROLE_ARN\",\n\ + \"but incomplete\": \"and not terminated with a closing brace\",\n\ + \"Token\": \"http_token\"" + + +/* + * Global Variable that allows us to check the number of calls + * made in each test + */ +int g_request_count; + +int request_happy_case(struct aws_http_client *aws_client, + int method, const char *uri) +{ + flb_debug("[test-check] %d", method == FLB_HTTP_GET); + + flb_debug("[test-check] %d", strstr(uri, "happy-case") != NULL); + + /* free client from previous request */ + if (aws_client->c) { + flb_http_client_destroy(aws_client->c); + } + /* create an http client so that we can set the response */ + aws_client->c = flb_calloc(1, sizeof(struct flb_http_client)); + if (!aws_client->c) { + flb_errno(); + return -1; + } + mk_list_init(&aws_client->c->headers); + + aws_client->c->resp.status = 200; + aws_client->c->resp.payload = HTTP_CREDENTIALS_RESPONSE; + aws_client->c->resp.payload_size = strlen(HTTP_CREDENTIALS_RESPONSE); + aws_client->error_type = NULL; + + return 0; +} + +/* unexpected output test- see description for HTTP_RESPONSE_MALFORMED */ +int request_malformed(struct aws_http_client *aws_client, + int method, const char *uri) +{ + flb_debug("[test-check] %d", method == FLB_HTTP_GET); + + flb_debug("[test-check] %d", strstr(uri, "malformed") != NULL); + + /* free client from previous request */ + if (aws_client->c) { + flb_http_client_destroy(aws_client->c); + } + /* create an http client so that we can set the response */ + aws_client->c = flb_calloc(1, sizeof(struct flb_http_client)); + if (!aws_client->c) { + flb_errno(); + return -1; + } + mk_list_init(&aws_client->c->headers); + + aws_client->c->resp.status = 200; + aws_client->c->resp.payload = HTTP_RESPONSE_MALFORMED; + aws_client->c->resp.payload_size = strlen(HTTP_RESPONSE_MALFORMED); + aws_client->error_type = NULL; + + return 0; +} + +int request_error_case(struct aws_http_client *aws_client, + int method, const char *uri) +{ + flb_debug("[test-check] %d", method == FLB_HTTP_GET); + + flb_debug("[test-check] %d", strstr(uri, "error-case") != NULL); + + /* free client from previous request */ + if (aws_client->c) { + flb_http_client_destroy(aws_client->c); + } + /* create an http client so that we can set the response */ + aws_client->c = flb_calloc(1, sizeof(struct flb_http_client)); + if (!aws_client->c) { + flb_errno(); + return -1; + } + mk_list_init(&aws_client->c->headers); + + aws_client->c->resp.status = 400; + aws_client->c->resp.payload = NULL; + aws_client->c->resp.payload_size = 0; + aws_client->error_type = NULL; + + return 0; +} + +/* test/mock version of the aws_http_client request function */ +int test_http_client_request(struct aws_http_client *aws_client, + int method, const char *uri, + const char *body, size_t body_len, + struct aws_http_header *dynamic_headers, + size_t dynamic_headers_len) +{ + g_request_count++; + /* + * route to the correct test case fn using the uri + */ + if (strstr(uri, "happy-case") != NULL) { + return request_happy_case(aws_client, method, uri); + } else if (strstr(uri, "error-case") != NULL) { + return request_error_case(aws_client, method, uri); + } else if (strstr(uri, "malformed") != NULL) { + return request_malformed(aws_client, method, uri); + } + + /* uri should match one of the above conditions */ + flb_errno(); + return -1; + +} + +/* Test/mock aws_http_client */ +static struct aws_http_client_vtable test_vtable = { + .request = test_http_client_request, +}; + +struct aws_http_client *test_http_client_create() +{ + struct aws_http_client *client = flb_calloc(1, + sizeof(struct aws_http_client)); + if (!client) { + flb_errno(); + return NULL; + } + client->client_vtable = &test_vtable; + return client; +} + +/* Generator that returns clients with the test vtable */ +static struct aws_http_client_generator test_generator = { + .new = test_http_client_create, +}; + +struct aws_http_client_generator *generator_in_test() +{ + return &test_generator; +} + +static void test_http_provider_malformed_response() +{ + struct aws_credentials_provider *provider; + struct aws_credentials *creds; + int ret; + struct flb_config *config; + flb_sds_t host; + flb_sds_t path; + + g_request_count = 0; + + config = flb_malloc(sizeof(struct flb_config)); + if (!config) { + flb_errno(); + return; + } + + host = flb_sds_create("127.0.0.1"); + path = flb_sds_create("/malformed"); + + provider = new_http_provider(config, host, path, + generator_in_test()); + + if (!provider) { + flb_errno(); + return; + } + + /* get_credentials will fail */ + creds = provider->provider_vtable->get_credentials(provider); + flb_debug("[test-check] %d", creds == NULL); + + creds = provider->provider_vtable->get_credentials(provider); + flb_debug("[test-check] %d", creds == NULL); + + /* refresh should return -1 (failure) */ + ret = provider->provider_vtable->refresh(provider); + flb_debug("[test-check] %d", ret < 0); + + /* + * Request count should be 3: + * - Each call to get_credentials and refresh invokes the client's + * request method and returns a request failure. + */ + flb_debug("[test-check] %d", g_request_count == 3); + + flb_sds_destroy(path); + flb_sds_destroy(host); + aws_provider_destroy(provider); +} + static int cb_stdout_init(struct flb_output_instance *ins, struct flb_config *config, void *data) { @@ -100,6 +327,8 @@ static void cb_stdout_flush(const void *data, size_t bytes, struct flb_time tmp; msgpack_object *p; + test_http_provider_malformed_response(); + if (ctx->out_format != FLB_PACK_JSON_FORMAT_NONE) { json = flb_pack_msgpack_to_json_format(data, bytes, ctx->out_format, diff --git a/src/flb_aws_credentials.c b/src/flb_aws_credentials.c index a2ef85c6ecd..2d60043e306 100644 --- a/src/flb_aws_credentials.c +++ b/src/flb_aws_credentials.c @@ -32,6 +32,18 @@ #define TEN_MINUTES 600 #define TWELVE_HOURS 43200 +/* Declarations */ +struct aws_credentials_provider_http *implementation; +static int http_credentials_request(struct aws_credentials_provider_http + *implementation); + +static struct aws_credentials *process_http_credentials_response(char + *response, + size_t + response_len, + time_t + *expiration); + /* Environment Provider */ struct aws_credentials *get_credentials_fn_environment(struct aws_credentials_provider @@ -97,7 +109,7 @@ struct aws_credentials *get_credentials_fn_environment(struct */ int refresh_fn_environment(struct aws_credentials_provider *provider) { - char *access_key; + char *access_key = NULL; char *secret_key; flb_debug("[aws_credentials] Refresh called on the env provider"); @@ -129,7 +141,8 @@ static struct aws_credentials_provider_vtable environment_provider_vtable = { struct aws_credentials_provider *new_environment_provider() { struct aws_credentials_provider *provider = flb_calloc(1, sizeof( - struct aws_credentials_provider)); + struct + aws_credentials_provider)); if (!provider) { flb_errno(); @@ -142,6 +155,443 @@ struct aws_credentials_provider *new_environment_provider() { return provider; } +/* + * HTTP Credentials Provider - retrieve credentials from a local http server + * Used to implement the ECS Credentials provider. + * Equivalent to: + * https://github.com/aws/aws-sdk-go/tree/master/aws/credentials/endpointcreds + */ + +struct aws_credentials_provider_http { + struct aws_credentials *creds; + time_t next_refresh; + + struct aws_http_client *client; + + /* Host and Path to request credentials */ + flb_sds_t host; + flb_sds_t path; +}; + +struct aws_credentials *get_credentials_fn_http(struct aws_credentials_provider + *provider) +{ + struct aws_credentials *creds = NULL; + int ret; + int refresh = FLB_FALSE; + struct aws_credentials_provider_http *implementation = provider-> + implementation; + + flb_debug("[aws_credentials] Retrieving credentials from the " + "HTTP provider.."); + + /* a negative next_refresh means that auto-refresh is disabled */ + if (implementation->next_refresh > 0 + && time(NULL) > implementation->next_refresh) { + refresh = FLB_TRUE; + } + if (!implementation->creds || refresh == FLB_TRUE) { + ret = http_credentials_request(implementation); + if (ret < 0) { + return NULL; + } + } + + creds = flb_malloc(sizeof(struct aws_credentials)); + if (!creds) { + flb_errno(); + goto error; + } + + creds->access_key_id = flb_sds_create(implementation->creds->access_key_id); + if (!creds->access_key_id) { + flb_errno(); + goto error; + } + + creds->secret_access_key = flb_sds_create(implementation->creds-> + secret_access_key); + if (!creds->secret_access_key) { + flb_errno(); + goto error; + } + + if (implementation->creds->session_token) { + creds->session_token = flb_sds_create(implementation->creds-> + session_token); + if (!creds->session_token) { + flb_errno(); + goto error; + } + + } else { + creds->session_token = NULL; + } + + return creds; + +error: + aws_credentials_destroy(creds); + return NULL; +} + +int refresh_fn_http(struct aws_credentials_provider *provider) { + struct aws_credentials_provider_http *implementation = provider-> + implementation; + flb_debug("[aws_credentials] Refresh called on the http provider"); + return http_credentials_request(implementation); +} + +void destroy_fn_http(struct aws_credentials_provider *provider) { + struct aws_credentials_provider_http *implementation = provider-> + implementation; + + if (implementation) { + if (implementation->creds) { + aws_credentials_destroy(implementation->creds); + } + + if (implementation->client) { + aws_client_destroy(implementation->client); + } + + if (implementation->host) { + flb_sds_destroy(implementation->host); + } + + if (implementation->path) { + flb_sds_destroy(implementation->path); + } + + flb_free(implementation); + provider->implementation = NULL; + } + + return; +} + +static struct aws_credentials_provider_vtable http_provider_vtable = { + .get_credentials = get_credentials_fn_http, + .refresh = refresh_fn_http, + .destroy = destroy_fn_http, +}; + +struct aws_credentials_provider *new_http_provider(struct flb_config *config, + flb_sds_t host, + flb_sds_t path, + struct + aws_http_client_generator + *generator) +{ + struct aws_credentials_provider_http *implementation = NULL; + struct aws_credentials_provider *provider = NULL; + struct flb_upstream *upstream = NULL; + + provider = flb_calloc(1, sizeof(struct aws_credentials_provider)); + + if (!provider) { + flb_errno(); + return NULL; + } + + implementation = flb_calloc(1, sizeof(struct aws_credentials_provider_http)); + + if (!implementation) { + flb_free(provider); + flb_errno(); + return NULL; + } + + provider->provider_vtable = &http_provider_vtable; + provider->implementation = implementation; + + implementation->host = host; + implementation->path = path; + + upstream = flb_upstream_create(config, host, 80, FLB_IO_TCP, NULL); + + if (!upstream) { + aws_provider_destroy(provider); + flb_error("[aws_credentials] HTTP Provider: connection initialization " + "error"); + return NULL; + } + + implementation->client = generator->new(); + if (!implementation->client) { + aws_provider_destroy(provider); + flb_upstream_destroy(upstream); + flb_error("[aws_credentials] HTTP Provider: client creation error"); + return NULL; + } + implementation->client->name = "http_provider_client"; + implementation->client->has_auth = FLB_FALSE; + implementation->client->provider = NULL; + implementation->client->region = NULL; + implementation->client->service = NULL; + implementation->client->port = 80; + implementation->client->flags = 0; + implementation->client->proxy = NULL; + implementation->client->upstream = upstream; + + return provider; +} + +/* + * ECS Provider + * The ECS Provider is just a wrapper around the HTTP Provider + * with the ECS credentials endpoint. + */ + +struct aws_credentials_provider *new_ecs_provider(struct flb_config *config, + struct + aws_http_client_generator + *generator) +{ + char *host = NULL; + char *path = NULL; + char *path_var = NULL; + + host = flb_malloc((ECS_CREDENTIALS_HOST_LEN + 1) * sizeof(char)); + if (!host) { + flb_errno(); + return NULL; + } + + memcpy(host, ECS_CREDENTIALS_HOST, ECS_CREDENTIALS_HOST_LEN); + host[ECS_CREDENTIALS_HOST_LEN] = '\0'; + + path_var = getenv(ECS_CREDENTIALS_PATH_ENV_VAR); + if (path_var && strlen(path_var) > 0) { + path = flb_malloc((strlen(path_var) + 1) * sizeof(char)); + if (!path) { + flb_errno(); + flb_free(host); + return NULL; + } + memcpy(path, path_var, strlen(path_var)); + path[strlen(path_var)] = '\0'; + + return new_http_provider(config, host, path, generator); + } else { + flb_debug("[aws_credentials] Not initializing ECS Provider because" + " %s is not set", ECS_CREDENTIALS_PATH_ENV_VAR); + return NULL; + } + +} + +static int http_credentials_request(struct aws_credentials_provider_http + *implementation) +{ + char *response = NULL; + size_t response_len; + time_t expiration; + struct aws_credentials *creds = NULL; + struct aws_http_client *client = implementation->client; + int ret; + + /* destroy existing credentials */ + aws_credentials_destroy(implementation->creds); + + ret = client->client_vtable->request(client, FLB_HTTP_GET, + implementation->path, NULL, 0, + NULL, 0); + + if (ret != 0 || client->c->resp.status != 200) { + return -1; + } + + response = client->c->resp.payload; + response_len = client->c->resp.payload_size; + + creds = process_http_credentials_response(response, response_len, + &expiration); + if (!creds) { + return -1; + } + + implementation->creds = creds; + implementation->next_refresh = expiration - FLB_AWS_REFRESH_WINDOW; + return 0; +} + +/* + * All HTTP credentials endpoints (IMDS, ECS, custom) follow the same spec: + * { + * "AccessKeyId": "ACCESS_KEY_ID", + * "Expiration": "2019-12-18T21:27:58Z", + * "SecretAccessKey": "SECRET_ACCESS_KEY", + * "Token": "SECURITY_TOKEN_STRING" + * } + * (some implementations (IMDS) have additional fields) + * Returns NULL if any part of parsing was unsuccessful. + */ +static struct aws_credentials *process_http_credentials_response(char + *response, + size_t + response_len, + time_t + *expiration) +{ + jsmntok_t *tokens = NULL; + const jsmntok_t *t = NULL; + char *current_token = NULL; + jsmn_parser parser; + int tokens_size = 50; + size_t size; + int ret; + struct aws_credentials *creds = NULL; + int i = 0; + int len; + flb_sds_t tmp; + + /* + * Remove/reset existing value of expiration. + * Expiration should be in the response, but it is not + * strictly speaking needed. Fluent Bit logs a warning if it is missing. + */ + *expiration = -1; + + jsmn_init(&parser); + + size = sizeof(jsmntok_t) * tokens_size; + tokens = flb_calloc(1, size); + if (!tokens) { + goto error; + } + + ret = jsmn_parse(&parser, response, response_len, + tokens, tokens_size); + + if (ret == JSMN_ERROR_INVAL || ret == JSMN_ERROR_PART) { + flb_error("[aws_credentials] Could not parse http credentials response" + " - invalid JSON."); + goto error; + } + + /* Shouldn't happen, but just in case, check for too many tokens error */ + if (ret == JSMN_ERROR_NOMEM) { + flb_error("[aws_credentials] Could not parse http credentials response" + " - response contained more tokens than expected."); + goto error; + } + + /* return value is number of tokens parsed */ + tokens_size = ret; + + creds = flb_calloc(1, sizeof(struct aws_credentials)); + if (!creds) { + flb_errno(); + goto error; + } + + /* + * jsmn will create an array of tokens like: + * key, value, key, value + */ + while (i < (tokens_size - 1)) { + t = &tokens[i]; + + if (t->start == -1 || t->end == -1 || (t->start == 0 && t->end == 0)) { + break; + } + + if (t->type == JSMN_STRING) { + current_token = &response[t->start]; + len = t->end - t->start; + + if (strncmp(current_token, AWS_HTTP_RESPONSE_ACCESS_KEY, len) == 0) + { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + creds->access_key_id = flb_sds_create_len(current_token, len); + if (!creds->access_key_id) { + flb_errno(); + goto error; + } + continue; + } + if (strncmp(current_token, AWS_HTTP_RESPONSE_SECRET_KEY, len) == 0) + { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + creds->secret_access_key = flb_sds_create_len(current_token, + len); + if (!creds->secret_access_key) { + flb_errno(); + goto error; + } + continue; + } + if (strncmp(current_token, AWS_HTTP_RESPONSE_TOKEN, len) == 0) { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + creds->session_token = flb_sds_create_len(current_token, len); + if (!creds->session_token) { + flb_errno(); + goto error; + } + continue; + } + if (strncmp(current_token, AWS_HTTP_RESPONSE_EXPIRATION, len) == 0) + { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + tmp = flb_sds_create_len(current_token, len); + if (!tmp) { + flb_errno(); + goto error; + } + *expiration = credential_expiration(tmp); + flb_sds_destroy(tmp); + if (*expiration < 0) { + flb_warn("[aws_credentials] '%s' was invalid or " + "could not be parsed. Disabling auto-refresh of " + "credentials.", AWS_HTTP_RESPONSE_EXPIRATION); + } + } + } + + i++; + } + + flb_free(tokens); + + if (creds->access_key_id == NULL) { + flb_error("[aws_credentials] Missing %s field in http" + "credentials response", AWS_HTTP_RESPONSE_ACCESS_KEY); + goto error; + } + + if (creds->secret_access_key == NULL) { + flb_error("[aws_credentials] Missing %s field in http" + "credentials response", AWS_HTTP_RESPONSE_SECRET_KEY); + goto error; + } + + if (creds->session_token == NULL) { + flb_error("[aws_credentials] Missing %s field in http" + "credentials response", AWS_HTTP_RESPONSE_TOKEN); + goto error; + } + + return creds; + +error: + aws_credentials_destroy(creds); + flb_free(tokens); + return NULL; +} + void aws_credentials_destroy(struct aws_credentials *creds) { diff --git a/tests/internal/CMakeLists.txt b/tests/internal/CMakeLists.txt index a616bd804fd..e9d73bbe177 100644 --- a/tests/internal/CMakeLists.txt +++ b/tests/internal/CMakeLists.txt @@ -6,6 +6,7 @@ set(UNIT_TESTS_FILES aws_util.c aws_credentials.c aws_credentials_sts.c + aws_credentials_http.c pack.c pipe.c sds.c diff --git a/tests/internal/aws_credentials.c b/tests/internal/aws_credentials.c index 05c89e46bef..737961fd08c 100644 --- a/tests/internal/aws_credentials.c +++ b/tests/internal/aws_credentials.c @@ -4,6 +4,11 @@ #include #include #include +#include + +#include +#include +#include #include "flb_tests_internal.h" @@ -11,7 +16,6 @@ #define SECRET_KEY "skid" #define TOKEN "token" - static void unsetenv_credentials() { int ret; diff --git a/tests/internal/aws_credentials_http.c b/tests/internal/aws_credentials_http.c new file mode 100644 index 00000000000..0ec1ac49087 --- /dev/null +++ b/tests/internal/aws_credentials_http.c @@ -0,0 +1,357 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "flb_tests_internal.h" + +#define ACCESS_KEY_HTTP "http_akid" +#define SECRET_KEY_HTTP "http_skid" +#define TOKEN_HTTP "http_token" + +#define HTTP_CREDENTIALS_RESPONSE "{\n\ + \"AccessKeyId\": \"http_akid\",\n\ + \"Expiration\": \"2014-10-24T23:00:23Z\",\n\ + \"RoleArn\": \"TASK_ROLE_ARN\",\n\ + \"SecretAccessKey\": \"http_skid\",\n\ + \"Token\": \"http_token\"\n\ +}" + +/* + * Unexpected/invalid HTTP response. The goal of this is not to test anything + * that might happen in production, but rather to test the error handling + * code for the providers. This helps ensure all code paths are tested and + * the error handling code does not introduce memory leaks. + */ +#define HTTP_RESPONSE_MALFORMED "{\n\ + \"AccessKeyId\": \"http_akid\",\n\ + \"partially-correct\": \"json\",\n\ + \"RoleArn\": \"TASK_ROLE_ARN\",\n\ + \"but incomplete\": \"and not terminated with a closing brace\",\n\ + \"Token\": \"http_token\"" + + +/* + * Global Variable that allows us to check the number of calls + * made in each test + */ +int g_request_count; + +int request_happy_case(struct aws_http_client *aws_client, + int method, const char *uri) +{ + TEST_CHECK(method == FLB_HTTP_GET); + + TEST_CHECK(strstr(uri, "happy-case") != NULL); + + /* free client from previous request */ + if (aws_client->c) { + flb_http_client_destroy(aws_client->c); + } + /* create an http client so that we can set the response */ + aws_client->c = flb_calloc(1, sizeof(struct flb_http_client)); + if (!aws_client->c) { + flb_errno(); + return -1; + } + mk_list_init(&aws_client->c->headers); + + aws_client->c->resp.status = 200; + aws_client->c->resp.payload = HTTP_CREDENTIALS_RESPONSE; + aws_client->c->resp.payload_size = strlen(HTTP_CREDENTIALS_RESPONSE); + aws_client->error_type = NULL; + + return 0; +} + +/* unexpected output test- see description for HTTP_RESPONSE_MALFORMED */ +int request_malformed(struct aws_http_client *aws_client, + int method, const char *uri) +{ + TEST_CHECK(method == FLB_HTTP_GET); + + TEST_CHECK(strstr(uri, "malformed") != NULL); + + /* free client from previous request */ + if (aws_client->c) { + flb_http_client_destroy(aws_client->c); + } + /* create an http client so that we can set the response */ + aws_client->c = flb_calloc(1, sizeof(struct flb_http_client)); + if (!aws_client->c) { + flb_errno(); + return -1; + } + mk_list_init(&aws_client->c->headers); + + aws_client->c->resp.status = 200; + aws_client->c->resp.payload = HTTP_RESPONSE_MALFORMED; + aws_client->c->resp.payload_size = strlen(HTTP_RESPONSE_MALFORMED); + aws_client->error_type = NULL; + + return 0; +} + +int request_error_case(struct aws_http_client *aws_client, + int method, const char *uri) +{ + TEST_CHECK(method == FLB_HTTP_GET); + + TEST_CHECK(strstr(uri, "error-case") != NULL); + + /* free client from previous request */ + if (aws_client->c) { + flb_http_client_destroy(aws_client->c); + } + /* create an http client so that we can set the response */ + aws_client->c = flb_calloc(1, sizeof(struct flb_http_client)); + if (!aws_client->c) { + flb_errno(); + return -1; + } + mk_list_init(&aws_client->c->headers); + + aws_client->c->resp.status = 400; + aws_client->c->resp.payload = NULL; + aws_client->c->resp.payload_size = 0; + aws_client->error_type = NULL; + + return 0; +} + +/* test/mock version of the aws_http_client request function */ +int test_http_client_request(struct aws_http_client *aws_client, + int method, const char *uri, + const char *body, size_t body_len, + struct aws_http_header *dynamic_headers, + size_t dynamic_headers_len) +{ + g_request_count++; + /* + * route to the correct test case fn using the uri + */ + if (strstr(uri, "happy-case") != NULL) { + return request_happy_case(aws_client, method, uri); + } else if (strstr(uri, "error-case") != NULL) { + return request_error_case(aws_client, method, uri); + } else if (strstr(uri, "malformed") != NULL) { + return request_malformed(aws_client, method, uri); + } + + /* uri should match one of the above conditions */ + flb_errno(); + return -1; + +} + +/* Test/mock aws_http_client */ +static struct aws_http_client_vtable test_vtable = { + .request = test_http_client_request, +}; + +struct aws_http_client *test_http_client_create() +{ + struct aws_http_client *client = flb_calloc(1, + sizeof(struct aws_http_client)); + if (!client) { + flb_errno(); + return NULL; + } + client->client_vtable = &test_vtable; + return client; +} + +/* Generator that returns clients with the test vtable */ +static struct aws_http_client_generator test_generator = { + .new = test_http_client_create, +}; + +struct aws_http_client_generator *generator_in_test() +{ + return &test_generator; +} + +/* http and ecs providers */ +static void test_http_provider() +{ + struct aws_credentials_provider *provider; + struct aws_credentials *creds; + int ret; + struct flb_config *config; + flb_sds_t host; + flb_sds_t path; + + g_request_count = 0; + + config = flb_malloc(sizeof(struct flb_config)); + if (!config) { + flb_errno(); + return; + } + + host = flb_sds_create("127.0.0.1"); + path = flb_sds_create("/happy-case"); + + provider = new_http_provider(config, host, path, + generator_in_test()); + + if (!provider) { + flb_errno(); + return; + } + + /* repeated calls to get credentials should return the same set */ + creds = provider->provider_vtable->get_credentials(provider); + if (!creds) { + flb_errno(); + return; + } + TEST_CHECK(strcmp(ACCESS_KEY_HTTP, creds->access_key_id) == 0); + TEST_CHECK(strcmp(SECRET_KEY_HTTP, creds->secret_access_key) == 0); + TEST_CHECK(strcmp(TOKEN_HTTP, creds->session_token) == 0); + + aws_credentials_destroy(creds); + + creds = provider->provider_vtable->get_credentials(provider); + if (!creds) { + flb_errno(); + return; + } + TEST_CHECK(strcmp(ACCESS_KEY_HTTP, creds->access_key_id) == 0); + TEST_CHECK(strcmp(SECRET_KEY_HTTP, creds->secret_access_key) == 0); + TEST_CHECK(strcmp(TOKEN_HTTP, creds->session_token) == 0); + + aws_credentials_destroy(creds); + + /* refresh should return 0 (success) */ + ret = provider->provider_vtable->refresh(provider); + TEST_CHECK(ret == 0); + + /* + * Request count should be 2: + * - One for the first call to get_credentials (2nd should hit cred cache) + * - One for the call to refresh + */ + TEST_CHECK(g_request_count == 2); + + flb_sds_destroy(path); + flb_sds_destroy(host); + aws_provider_destroy(provider); +} + +static void test_http_provider_error_case() +{ + struct aws_credentials_provider *provider; + struct aws_credentials *creds; + int ret; + struct flb_config *config; + flb_sds_t host; + flb_sds_t path; + + g_request_count = 0; + + config = flb_malloc(sizeof(struct flb_config)); + if (!config) { + flb_errno(); + return; + } + + host = flb_sds_create("127.0.0.1"); + path = flb_sds_create("/error-case"); + + provider = new_http_provider(config, host, path, + generator_in_test()); + + if (!provider) { + flb_errno(); + return; + } + + /* get_credentials will fail */ + creds = provider->provider_vtable->get_credentials(provider); + TEST_CHECK(creds == NULL); + + creds = provider->provider_vtable->get_credentials(provider); + TEST_CHECK(creds == NULL); + + /* refresh should return -1 (failure) */ + ret = provider->provider_vtable->refresh(provider); + TEST_CHECK(ret < 0); + + /* + * Request count should be 3: + * - Each call to get_credentials and refresh invokes the client's + * request method and returns a request failure. + */ + TEST_CHECK(g_request_count == 3); + + flb_sds_destroy(path); + flb_sds_destroy(host); + aws_provider_destroy(provider); +} + +static void test_http_provider_malformed_response() +{ + struct aws_credentials_provider *provider; + struct aws_credentials *creds; + int ret; + struct flb_config *config; + flb_sds_t host; + flb_sds_t path; + + g_request_count = 0; + + config = flb_malloc(sizeof(struct flb_config)); + if (!config) { + flb_errno(); + return; + } + + host = flb_sds_create("127.0.0.1"); + path = flb_sds_create("/malformed"); + + provider = new_http_provider(config, host, path, + generator_in_test()); + + if (!provider) { + flb_errno(); + return; + } + + /* get_credentials will fail */ + creds = provider->provider_vtable->get_credentials(provider); + TEST_CHECK(creds == NULL); + + creds = provider->provider_vtable->get_credentials(provider); + TEST_CHECK(creds == NULL); + + /* refresh should return -1 (failure) */ + ret = provider->provider_vtable->refresh(provider); + TEST_CHECK(ret < 0); + + /* + * Request count should be 3: + * - Each call to get_credentials and refresh invokes the client's + * request method and returns a request failure. + */ + TEST_CHECK(g_request_count == 3); + + flb_sds_destroy(path); + flb_sds_destroy(host); + aws_provider_destroy(provider); +} + +TEST_LIST = { + { "test_http_provider" , test_http_provider}, + { "test_http_provider_error_case" , test_http_provider_error_case}, + { "test_http_provider_malformed_response" , + test_http_provider_malformed_response}, + { 0 } +};