diff --git a/src/aws/flb_aws_credentials_http.c b/src/aws/flb_aws_credentials_http.c index 90e764e6d9d..8a490eb773b 100644 --- a/src/aws/flb_aws_credentials_http.c +++ b/src/aws/flb_aws_credentials_http.c @@ -23,6 +23,11 @@ #include #include #include +#include + +#ifdef FLB_SYSTEM_WINDOWS +#include +#endif #include #include @@ -63,22 +68,44 @@ does not satisfy any of the following conditions: is within the loopback CIDR (IPv4 127.0.0.0/8, IPv6 ::1/128) is the ECS container host 169.254.170.2 is the EKS container host (IPv4 169.254.170.23, IPv6 fd00:ec2::23) -is localhost with Greengrass credential provider path (/2016-11-01/credentialprovider/)*/ +is localhost with Greengrass credential provider path (/2016-11-01/credentialprovider/) +or is an IPv6 link-local address (fe80::/10). */ static int validate_http_credential_uri(flb_sds_t protocol, flb_sds_t host, flb_sds_t path) { - if (strncmp(protocol, "https", 5) == 0) { + struct in_addr ipv4; + struct in6_addr ipv6; + struct in6_addr eks_ipv6; + + if (strcmp(protocol, "https") == 0) { return 0; - } else if (strncmp(host, "127.", 4) == 0 || - strncmp(host, ECS_CREDENTIALS_HOST, ECS_CREDENTIALS_HOST_LEN) == 0 || - strncmp(host, EKS_CREDENTIALS_HOST, EKS_CREDENTIALS_HOST_LEN) == 0 || - strstr(host, "::1") != NULL || - strstr(host, "fd00:ec2::23") != NULL || - strstr(host, "fe80:") != NULL || - (strcmp(host, GREENGRASS_CREDENTIALS_HOST) == 0 && path != NULL && - strcmp(path, GREENGRASS_CREDENTIALS_PATH) == 0)) { + } + + if (strcmp(protocol, "http") != 0) { + return -1; + } + + if (strcmp(host, GREENGRASS_CREDENTIALS_HOST) == 0 && path != NULL && + strcmp(path, GREENGRASS_CREDENTIALS_PATH) == 0) { return 0; } + if (inet_pton(AF_INET, host, &ipv4) == 1) { + if (((unsigned char *) &ipv4.s_addr)[0] == 127 || + strcmp(host, ECS_CREDENTIALS_HOST) == 0 || + strcmp(host, EKS_CREDENTIALS_HOST) == 0) { + return 0; + } + } + + if (inet_pton(AF_INET6, host, &ipv6) == 1) { + inet_pton(AF_INET6, "fd00:ec2::23", &eks_ipv6); + if (IN6_IS_ADDR_LOOPBACK(&ipv6) || + memcmp(&ipv6, &eks_ipv6, sizeof(ipv6)) == 0 || + (ipv6.s6_addr[0] == 0xfe && (ipv6.s6_addr[1] & 0xc0) == 0x80)) { + return 0; + } + } + return -1; } @@ -360,7 +387,7 @@ struct flb_aws_provider *flb_http_provider_create(struct flb_config *config, return NULL; } - insecure = strncmp(protocol, "http", 4) == 0 ? FLB_TRUE : FLB_FALSE; + insecure = strcmp(protocol, "http") == 0 ? FLB_TRUE : FLB_FALSE; ret = validate_http_credential_uri(protocol, host, path); if (ret < 0) { flb_error("[aws credentials] %s must be set to an https:// address, a link local IP address, " @@ -373,6 +400,18 @@ struct flb_aws_provider *flb_http_provider_create(struct flb_config *config, flb_sds_destroy(path); return NULL; } + + /* Use a numeric loopback address so localhost cannot resolve elsewhere. */ + if (insecure == FLB_TRUE && strcmp(host, GREENGRASS_CREDENTIALS_HOST) == 0) { + flb_sds_destroy(host); + host = flb_sds_create("127.0.0.1"); + if (host == NULL) { + flb_sds_destroy(protocol); + flb_sds_destroy(port_sds); + flb_sds_destroy(path); + return NULL; + } + } } else { flb_debug("[aws_credentials] Not initializing ECS/EKS HTTP Provider because" diff --git a/tests/include/aws_client_mock.h b/tests/include/aws_client_mock.h index d0d00a85cd0..4e88e085851 100644 --- a/tests/include/aws_client_mock.h +++ b/tests/include/aws_client_mock.h @@ -67,7 +67,40 @@ #define AWS_CLIENT_MOCK_H /* Variadic Argument Counter, Counts up to 64 variadic args */ -#ifdef _MSC_VER +#if defined(_MSC_VER) && defined(_MSVC_TRADITIONAL) && _MSVC_TRADITIONAL +/* Detect empty arguments before counting with the traditional preprocessor. */ +#define FLB_AWS_CLIENT_MOCK_COUNT_T(args) \ + FLB_AWS_CLIENT_MOCK_EVAL(_FLB_AWS_CLIENT_MOCK_COUNT64 args) +#define FLB_AWS_CLIENT_MOCK_HAS_COMMA(...) \ + FLB_AWS_CLIENT_MOCK_EVAL(FLB_AWS_CLIENT_MOCK_COUNT_T((dummy, __VA_ARGS__, 1, 1, 1, 1, 1, 1, \ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0))) +#define FLB_AWS_CLIENT_MOCK_TRIGGER(...) , +#define FLB_AWS_CLIENT_MOCK_JOIN_I(a,b,c,d,e) a##b##c##d##e +#define FLB_AWS_CLIENT_MOCK_JOIN(a,b,c,d,e) \ + FLB_AWS_CLIENT_MOCK_EVAL(FLB_AWS_CLIENT_MOCK_JOIN_I(a, b, c, d, e)) +#define FLB_AWS_CLIENT_MOCK_EMPTY_CASE_0001 , +#define FLB_AWS_CLIENT_MOCK_EMPTY_I(a,b,c,d) \ + FLB_AWS_CLIENT_MOCK_EVAL(FLB_AWS_CLIENT_MOCK_HAS_COMMA(FLB_AWS_CLIENT_MOCK_JOIN(FLB_AWS_CLIENT_MOCK_EMPTY_CASE_, \ + a, b, c, d))) +#define FLB_AWS_CLIENT_MOCK_IS_EMPTY(...) \ + FLB_AWS_CLIENT_MOCK_EVAL(FLB_AWS_CLIENT_MOCK_EMPTY_I(FLB_AWS_CLIENT_MOCK_HAS_COMMA(__VA_ARGS__), \ + FLB_AWS_CLIENT_MOCK_HAS_COMMA(FLB_AWS_CLIENT_MOCK_TRIGGER __VA_ARGS__), \ + FLB_AWS_CLIENT_MOCK_HAS_COMMA(__VA_ARGS__ ()), \ + FLB_AWS_CLIENT_MOCK_HAS_COMMA(FLB_AWS_CLIENT_MOCK_TRIGGER __VA_ARGS__ ()))) +#define FLB_AWS_CLIENT_MOCK_CAT_I(a,b) a##b +#define FLB_AWS_CLIENT_MOCK_CAT(a,b) FLB_AWS_CLIENT_MOCK_CAT_I(a,b) +#define FLB_AWS_CLIENT_MOCK_COUNT_1(...) 0 +#define FLB_AWS_CLIENT_MOCK_COUNT_0(...) \ + FLB_AWS_CLIENT_MOCK_COUNT_T((dummy, __VA_ARGS__, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, \ + 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, \ + 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, \ + 5, 4, 3, 2, 1, 0)) +#define FLB_AWS_CLIENT_MOCK_COUNT(...) \ + FLB_AWS_CLIENT_MOCK_EVAL(FLB_AWS_CLIENT_MOCK_CAT(FLB_AWS_CLIENT_MOCK_COUNT_, \ + FLB_AWS_CLIENT_MOCK_IS_EMPTY(__VA_ARGS__))(__VA_ARGS__)) +#define FLB_AWS_CLIENT_MOCK_COUNT64(...) FLB_AWS_CLIENT_MOCK_COUNT(__VA_ARGS__) +#elif defined(_MSC_VER) #define FLB_AWS_CLIENT_MOCK_COUNT64(...) \ _FLB_AWS_CLIENT_MOCK_COUNT64(dummy __VA_OPT__(,) __VA_ARGS__, 63, 62, 61, 60, 59, \ 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, \ @@ -97,12 +130,14 @@ /* Make block-scope addressable compound-literal request chain */ #define FLB_AWS_CLIENT_MOCK(...) \ FLB_AWS_CLIENT_MOCK_EVAL(&(struct flb_aws_client_mock_request_chain){ \ - FLB_AWS_CLIENT_MOCK_COUNT64(__VA_ARGS__), \ + sizeof((struct flb_aws_client_mock_response[]){__VA_ARGS__}) / \ + sizeof(struct flb_aws_client_mock_response), \ (struct flb_aws_client_mock_response[]){__VA_ARGS__}}) #define FLB_AWS_CLIENT_MOCK_RESPONSE(...) \ { \ - FLB_AWS_CLIENT_MOCK_COUNT64(__VA_ARGS__), \ + sizeof((struct flb_aws_client_mock_response_config[]){__VA_ARGS__}) / \ + sizeof(struct flb_aws_client_mock_response_config), \ (struct flb_aws_client_mock_response_config[]) \ { \ __VA_ARGS__ \ diff --git a/tests/integration/src/utils/fluent_bit_manager.py b/tests/integration/src/utils/fluent_bit_manager.py index 55a90bf18ca..cfb1bc96454 100644 --- a/tests/integration/src/utils/fluent_bit_manager.py +++ b/tests/integration/src/utils/fluent_bit_manager.py @@ -23,6 +23,7 @@ import signal import shutil import subprocess +import sys import tempfile import time @@ -46,6 +47,7 @@ logger = logging.getLogger(__name__) FLUENT_BIT_VERSION_PATTERN = re.compile(r"(?:Fluent Bit\s+)?v?(\d+)\.(\d+)\.(\d+)") _BINARY_VERSION_INFO_CACHE = {} +_WINDOWS_ATTACH_CONSOLE_FAILED = 3 class FluentBitStartupError(RuntimeError): pass @@ -234,10 +236,20 @@ def start(self): output_path = self.leaks_log_file if leaks_enabled() else self.log_file self.output_handle = open(output_path, "a", encoding="utf-8") + process_options = {} + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + process_options = { + "creationflags": subprocess.CREATE_NEW_CONSOLE, + "startupinfo": startupinfo, + } self.process = subprocess.Popen( command, stdout=self.output_handle, stderr=subprocess.STDOUT, + **process_options, ) if leaks_enabled(): @@ -257,10 +269,21 @@ def stop(self): return pid = self.target_pid or self.process.pid + shutdown_error = None return_code = self.process.poll() supervisor_running = self.process.poll() is None if supervisor_running or (leaks_enabled() and self.target_pid): - self.send_signal(signal.SIGTERM) + if os.name == "nt": + try: + self._send_windows_shutdown(pid) + except subprocess.TimeoutExpired: + logger.warning("Windows shutdown helper timed out; waiting for Fluent Bit") + except subprocess.CalledProcessError as error: + if (error.returncode != _WINDOWS_ATTACH_CONSOLE_FAILED + or self.process.poll() is None): + shutdown_error = error + else: + self.send_signal(signal.SIGTERM) if supervisor_running: try: @@ -280,6 +303,9 @@ def stop(self): self.output_handle.close() self.output_handle = None + if shutdown_error is not None: + raise shutdown_error + if self.http_monitoring_port: timeout = 10 if memory_check_enabled() else 5 wait_for_port_to_be_free(int(self.http_monitoring_port), timeout=timeout) @@ -292,6 +318,32 @@ def stop(self): logger.info(f"Fluent Bit stopped (pid: {pid})") + @staticmethod + def _send_windows_shutdown(pid): + # Attach a helper to the child's isolated console and send Ctrl+C. + # CTRL_BREAK triggers Fluent Bit reload; SIGTERM forcibly kills it. + helper = f""" +import ctypes +import sys +import time + +k = ctypes.WinDLL('kernel32', use_last_error=True) +k.FreeConsole() +if not k.AttachConsole(int(sys.argv[1])): + print('AttachConsole failed:', ctypes.get_last_error(), file=sys.stderr) + sys.exit({_WINDOWS_ATTACH_CONSOLE_FAILED}) +try: + if not k.SetConsoleCtrlHandler(None, True): + raise ctypes.WinError(ctypes.get_last_error()) + if not k.GenerateConsoleCtrlEvent(0, 0): + raise ctypes.WinError(ctypes.get_last_error()) + time.sleep(0.1) +finally: + k.FreeConsole() +""" + subprocess.run([sys.executable, "-c", helper, str(pid)], check=True, + creationflags=subprocess.CREATE_NO_WINDOW, timeout=5) + def send_signal(self, signal_number): if not self.process: raise RuntimeError("Fluent Bit is not running") diff --git a/tests/integration/tests/test_fluent_bit_manager.py b/tests/integration/tests/test_fluent_bit_manager.py index 7026acac397..5f37e5d11fb 100644 --- a/tests/integration/tests/test_fluent_bit_manager.py +++ b/tests/integration/tests/test_fluent_bit_manager.py @@ -15,6 +15,9 @@ # limitations under the License. import signal +import subprocess +import sys +from types import SimpleNamespace from unittest.mock import Mock import pytest @@ -150,9 +153,10 @@ def test_send_signal_raises_when_process_is_missing(): manager = FluentBitManager("/tmp/fluent-bit.yaml") with pytest.raises(RuntimeError, match="not running"): - manager.send_signal(signal.SIGHUP) + manager.send_signal(signal.SIGTERM) +@pytest.mark.skipif(not hasattr(signal, "SIGHUP"), reason="SIGHUP is unavailable on Windows") def test_send_sighup_forwards_signal_to_process(): manager = FluentBitManager("/tmp/fluent-bit.yaml") manager.process = Mock() @@ -170,10 +174,16 @@ def test_stop_uses_configured_shutdown_timeout(monkeypatch, configured, expected process = Mock() process.poll.return_value = None manager.process = process + windows_shutdown = Mock() + monkeypatch.setattr(manager, "_send_windows_shutdown", windows_shutdown) manager.stop() - process.send_signal.assert_called_once_with(signal.SIGTERM) + if manager_module.os.name == "nt": + windows_shutdown.assert_called_once_with(process.pid) + process.send_signal.assert_not_called() + else: + process.send_signal.assert_called_once_with(signal.SIGTERM) process.wait.assert_called_once_with(timeout=expected) @@ -227,7 +237,7 @@ def fake_create_results_directory(self, base_dir='results'): monkeypatch.setattr(FluentBitManager, "create_results_directory", fake_create_results_directory) monkeypatch.setattr(FluentBitManager, "get_version_info", lambda self: ("vtest", "commit")) - def fake_popen(command, stdout=None, stderr=None): + def fake_popen(command, stdout=None, stderr=None, **kwargs): popen_calls.append(command) return popen_result @@ -243,7 +253,7 @@ def fake_popen(command, stdout=None, stderr=None): f"--log-file={manager.valgrind_log_file}", "--leak-check=full", "--show-leak-kinds=all", - "/usr/bin/fluent-bit", + manager.binary_absolute_path, "-c", str(config_path), "-l", str(tmp_path / "run-1" / "fluent_bit.log") ]] @@ -330,3 +340,120 @@ def test_wait_for_hot_reload_count_times_out(monkeypatch): with pytest.raises(TimeoutError, match="Timed out waiting for hot reload count 1"): manager.wait_for_hot_reload_count(1, timeout=0.25) + + +@pytest.mark.parametrize("failed_api", [None, "AttachConsole", "SetConsoleCtrlHandler", + "GenerateConsoleCtrlEvent"]) +def test_windows_shutdown_helper_runs_with_optimization(monkeypatch, failed_api, capsys): + run = Mock() + monkeypatch.setattr(manager_module.subprocess, "run", run) + monkeypatch.setattr(manager_module.subprocess, "CREATE_NO_WINDOW", 0, raising=False) + FluentBitManager._send_windows_shutdown(1234) + assert run.call_args.kwargs["timeout"] == 5 + helper = run.call_args.args[0][2] + kernel = Mock() + for name in ("AttachConsole", "SetConsoleCtrlHandler", "GenerateConsoleCtrlEvent"): + getattr(kernel, name).return_value = name != failed_api + get_last_error = Mock(return_value=5) + ctypes_stub = SimpleNamespace( + WinDLL=Mock(return_value=kernel), get_last_error=get_last_error, + WinError=lambda code: RuntimeError(f"Windows error {code}"), + ) + monkeypatch.setitem(sys.modules, "ctypes", ctypes_stub) + monkeypatch.setattr(sys, "argv", ["-c", "1234"]) + monkeypatch.setattr(manager_module.time, "sleep", Mock()) + + optimized_helper = compile(helper, "", "exec", optimize=2) + if failed_api == "AttachConsole": + with pytest.raises(SystemExit) as error: + exec(optimized_helper, {}) + assert error.value.code == manager_module._WINDOWS_ATTACH_CONSOLE_FAILED + assert "AttachConsole failed: 5" in capsys.readouterr().err + elif failed_api: + with pytest.raises(RuntimeError, match="Windows error 5"): + exec(optimized_helper, {}) + else: + exec(optimized_helper, {}) + + kernel.AttachConsole.assert_called_once_with(1234) + if failed_api != "AttachConsole": + kernel.SetConsoleCtrlHandler.assert_called_once_with(None, True) + else: + kernel.SetConsoleCtrlHandler.assert_not_called() + if failed_api not in ("AttachConsole", "SetConsoleCtrlHandler"): + kernel.GenerateConsoleCtrlEvent.assert_called_once_with(0, 0) + else: + kernel.GenerateConsoleCtrlEvent.assert_not_called() + assert kernel.FreeConsole.call_count == (1 if failed_api == "AttachConsole" else 2) + assert get_last_error.call_count == (1 if failed_api else 0) + + +@pytest.mark.skipif(manager_module.os.name != "nt", reason="Windows console shutdown") +@pytest.mark.parametrize("error_code,exited,raises", [ + (manager_module._WINDOWS_ATTACH_CONSOLE_FAILED, True, False), + (manager_module._WINDOWS_ATTACH_CONSOLE_FAILED, False, True), + (1, True, True), + (1, False, True), +]) +def test_windows_shutdown_failure_cleans_up(monkeypatch, error_code, exited, raises): + monkeypatch.delenv("VALGRIND", raising=False) + monkeypatch.delenv("LEAKS", raising=False) + manager = FluentBitManager("/tmp/fluent-bit.yaml", shutdown_timeout=1) + process = Mock() + process.poll.side_effect = [None, None, 0 if exited else None] + if exited: + process.wait.return_value = 0 + else: + process.wait.side_effect = subprocess.TimeoutExpired("fluent-bit", 1) + manager.process = process + manager.target_pid = 1234 + handle = manager.output_handle = Mock() + helper_error = subprocess.CalledProcessError(error_code, "shutdown-helper") + monkeypatch.setattr(manager, "_send_windows_shutdown", Mock(side_effect=helper_error)) + force_stop = Mock() + monkeypatch.setattr(manager, "_force_stop", force_stop) + + if raises: + with pytest.raises(subprocess.CalledProcessError) as error: + manager.stop() + assert error.value is helper_error + else: + manager.stop() + + assert manager.process is None + assert manager.target_pid is None + assert manager.output_handle is None + handle.close.assert_called_once() + assert force_stop.call_count == (0 if exited else 1) + if error_code == manager_module._WINDOWS_ATTACH_CONSOLE_FAILED: + assert process.poll.call_count == 3 + + +@pytest.mark.skipif(manager_module.os.name != "nt", reason="Windows console shutdown") +@pytest.mark.parametrize("exited", [True, False]) +def test_windows_shutdown_helper_timeout_reaches_cleanup(monkeypatch, exited): + monkeypatch.delenv("VALGRIND", raising=False) + monkeypatch.delenv("LEAKS", raising=False) + manager = FluentBitManager("/tmp/fluent-bit.yaml", shutdown_timeout=1) + process = Mock() + process.poll.return_value = None + if exited: + process.wait.return_value = 0 + else: + process.wait.side_effect = subprocess.TimeoutExpired("fluent-bit", 1) + manager.process = process + manager.target_pid = 1234 + handle = manager.output_handle = Mock() + monkeypatch.setattr(manager, "_send_windows_shutdown", + Mock(side_effect=subprocess.TimeoutExpired("shutdown-helper", 5))) + force_stop = Mock() + monkeypatch.setattr(manager, "_force_stop", force_stop) + + manager.stop() + + process.wait.assert_called_once_with(timeout=1) + assert force_stop.call_count == (0 if exited else 1) + assert manager.process is None + assert manager.target_pid is None + assert manager.output_handle is None + handle.close.assert_called_once() diff --git a/tests/internal/aws_credentials_http.c b/tests/internal/aws_credentials_http.c index cdcb13e939c..09dc1050989 100644 --- a/tests/internal/aws_credentials_http.c +++ b/tests/internal/aws_credentials_http.c @@ -8,14 +8,29 @@ #include #include +#ifndef FLB_SYSTEM_WINDOWS #include +#endif #include "flb_tests_internal.h" #include "../include/aws_client_mock.h" #include "../include/aws_client_mock.c" -#include "aws_credentials_test_internal.h" +#define AWS_TEST_DATA_PATH(FILE_PATH) FLB_TESTS_DATA_PATH "data/aws_credentials/" FILE_PATH + +#ifdef FLB_SYSTEM_WINDOWS +static int flb_test_setenv(const char *name, const char *value, int overwrite) +{ + if (overwrite == 0 && getenv(name) != NULL) { + return 0; + } + + return _putenv_s(name, value); +} +#else +#define flb_test_setenv(name, value, overwrite) setenv(name, value, overwrite) +#endif #define ACCESS_KEY_HTTP "http_akid" #define SECRET_KEY_HTTP "http_skid" @@ -23,7 +38,7 @@ #define HTTP_CREDENTIALS_RESPONSE "{\n\ \"AccessKeyId\": \"http_akid\",\n\ - \"Expiration\": \"2025-10-24T23:00:23Z\",\n\ + \"Expiration\": \"2099-10-24T23:00:23Z\",\n\ \"RoleArn\": \"TASK_ROLE_ARN\",\n\ \"SecretAccessKey\": \"http_skid\",\n\ \"Token\": \"http_token\"\n\ @@ -184,6 +199,8 @@ static void test_http_provider() int ret; struct flb_config *config; + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/happy-case", 1); + g_request_count = 0; config = flb_config_init(); @@ -194,11 +211,7 @@ static void test_http_provider() provider = flb_http_provider_create(config, generator_in_test()); - if (!provider) { - flb_errno(); - flb_config_exit(config); - return; - } + TEST_ASSERT(provider != NULL); /* repeated calls to get credentials should return the same set */ creds = provider->provider_vtable->get_credentials(provider); @@ -247,6 +260,8 @@ static void test_http_provider_error_case() int ret; struct flb_config *config; + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/error-case", 1); + g_request_count = 0; config = flb_config_init(); @@ -256,11 +271,7 @@ static void test_http_provider_error_case() } provider = flb_http_provider_create(config, generator_in_test()); - if (!provider) { - flb_errno(); - flb_config_exit(config); - return; - } + TEST_ASSERT(provider != NULL); /* get_credentials will fail */ creds = provider->provider_vtable->get_credentials(provider); @@ -292,6 +303,8 @@ static void test_http_provider_malformed_response() int ret; struct flb_config *config; + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/malformed", 1); + g_request_count = 0; config = flb_config_init(); @@ -304,11 +317,7 @@ static void test_http_provider_malformed_response() provider = flb_http_provider_create(config, generator_in_test()); - if (!provider) { - flb_errno(); - flb_config_exit(config); - return; - } + TEST_ASSERT(provider != NULL); /* get_credentials will fail */ creds = provider->provider_vtable->get_credentials(provider); @@ -375,7 +384,7 @@ static void test_http_provider_ecs_case() struct flb_config *config; int ret; - setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/iam_credentials/pod1", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/iam_credentials/pod1", 1); setup_test(FLB_AWS_CLIENT_MOCK( response( @@ -386,7 +395,7 @@ static void test_http_provider_ecs_case() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"XACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"XSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"XTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ), response( @@ -396,7 +405,7 @@ static void test_http_provider_ecs_case() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"YACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"YSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"YTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), // Expires Year 3021 + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), /* Expires in 2099. */ set(PAYLOAD_SIZE, 257) ) ), &provider, &config); @@ -447,8 +456,8 @@ static void test_http_provider_eks_with_token() struct flb_config *config; int ret; - setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/iam_credentials/pod1", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "password", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/iam_credentials/pod1", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "password", 1); setup_test(FLB_AWS_CLIENT_MOCK( response( @@ -459,7 +468,7 @@ static void test_http_provider_eks_with_token() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"XACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"XSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"XTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ), response( @@ -470,7 +479,7 @@ static void test_http_provider_eks_with_token() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"YACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"YSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"YTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ) ), &provider, &config); @@ -522,8 +531,8 @@ static void test_http_provider_eks_with_token_file() int ret; /* tests validation of valid non-default local loopback IP */ - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://127.0.0.7:80/iam_credentials/pod1", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", TEST_AUTHORIZATION_TOKEN_FILE, 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://127.0.0.7:80/iam_credentials/pod1", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", TEST_AUTHORIZATION_TOKEN_FILE, 1); setup_test(FLB_AWS_CLIENT_MOCK( response( @@ -534,7 +543,7 @@ static void test_http_provider_eks_with_token_file() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"XACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"XSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"XTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ), response( @@ -545,7 +554,7 @@ static void test_http_provider_eks_with_token_file() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"YACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"YSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"YTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ) ), &provider, &config); @@ -597,8 +606,8 @@ static void test_http_provider_https_endpoint() struct flb_config *config; int ret; - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://customers-vpc-credential-vending-server/iam_credentials/pod1", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", TEST_AUTHORIZATION_TOKEN_FILE, 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://customers-vpc-credential-vending-server/iam_credentials/pod1", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", TEST_AUTHORIZATION_TOKEN_FILE, 1); setup_test(FLB_AWS_CLIENT_MOCK( response( @@ -609,7 +618,7 @@ static void test_http_provider_https_endpoint() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"XACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"XSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"XTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ), response( @@ -620,7 +629,7 @@ static void test_http_provider_https_endpoint() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"YACCESSEKSXXX\",\n \"SecretAccessKey\"" " : \"YSECRETEKSXXXXXXXXXXXXXX\",\n \"Token\" : \"YTOKENEKSXXXXXXXXXXXXXXX==\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ) ), &provider, &config); @@ -671,8 +680,8 @@ static void test_http_provider_server_failure() struct flb_config *config; int ret; - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://customers-vpc-credential-vending-server/iam_credentials/pod1", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", TEST_AUTHORIZATION_TOKEN_FILE, 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://customers-vpc-credential-vending-server/iam_credentials/pod1", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", TEST_AUTHORIZATION_TOKEN_FILE, 1); setup_test(FLB_AWS_CLIENT_MOCK( response( @@ -714,8 +723,8 @@ static void test_http_validator_invalid_host() struct flb_aws_provider *provider; struct flb_config *config; - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://104.156.107.142:80/iam_credentials/pod1", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "password", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://104.156.107.142:80/iam_credentials/pod1", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "password", 1); flb_aws_client_mock_configure_generator(NULL); @@ -731,13 +740,80 @@ static void test_http_validator_invalid_host() flb_free(config); } +static void test_http_validator_rejects_host_lookalikes() +{ + const char *uris[] = { + "http://127.attacker.example/credentials", + "http://127.1.attacker.example/credentials", + "http://169.254.170.2.attacker.example/credentials", + "http://169.254.170.23.attacker.example/credentials", + "http://localhost.attacker.example/2016-11-01/credentialprovider/", + "http://169.254.170.20/credentials", + "http://[::1:2]/credentials", + "http://[fd00:ec2::23:1]/credentials", + "http://[::ffff:127.0.0.1]/credentials", + "http://[2001:db8:fe80::1]/credentials", + "httpx://127.0.0.1/credentials", + NULL + }; + struct flb_aws_provider *provider; + struct flb_config *config; + int index; + + flb_aws_client_mock_configure_generator(NULL); + + config = flb_calloc(1, sizeof(struct flb_config)); + TEST_ASSERT(config != NULL); + mk_list_init(&config->upstreams); + + for (index = 0; uris[index] != NULL; index++) { + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", uris[index], 1); + provider = flb_http_provider_create(config, flb_aws_client_get_mock_generator()); + TEST_CHECK(provider == NULL); + if (provider != NULL) { + flb_aws_provider_destroy(provider); + } + } + + flb_aws_client_mock_destroy_generator(); + flb_free(config); +} + +static void test_http_validator_accepts_allowed_addresses() +{ + const char *uris[] = { + "http://127.0.0.7/credentials", + "http://169.254.170.2/credentials", + "http://169.254.170.23/credentials", + "http://[::1]/credentials", + "http://[fd00:ec2::23]/credentials", + "http://[fe80::1]/credentials", + NULL + }; + struct flb_aws_provider *provider; + struct flb_config *config; + int index; + + config = flb_config_init(); + TEST_ASSERT(config != NULL); + + for (index = 0; uris[index] != NULL; index++) { + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", uris[index], 1); + provider = flb_http_provider_create(config, generator_in_test()); + TEST_ASSERT(provider != NULL); + flb_aws_provider_destroy(provider); + } + + flb_config_exit(config); +} + static void test_http_validator_invalid_port() { struct flb_aws_provider *provider; struct flb_config *config; - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://104.156.107.142:AA/iam_credentials/pod1", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "password", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://104.156.107.142:AA/iam_credentials/pod1", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "password", 1); flb_aws_client_mock_configure_generator(NULL); @@ -758,12 +834,13 @@ static void test_http_validator_invalid_port() static void test_http_validator_greengrass_valid() { struct flb_aws_provider *provider; + struct flb_aws_provider_http *implementation; struct flb_aws_credentials *creds; struct flb_config *config; /* Test valid Greengrass URL with localhost and correct path */ - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:8080/2016-11-01/credentialprovider/", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:8080/2016-11-01/credentialprovider/", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); setup_test(FLB_AWS_CLIENT_MOCK( response( @@ -774,11 +851,14 @@ static void test_http_validator_greengrass_valid() set(PAYLOAD, "{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-16T18:29:09Z\",\n" " \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"GREENGRASSACCESSKEY\",\n \"SecretAccessKey\"" " : \"GREENGRASSSECRETKEY\",\n \"Token\" : \"GREENGRASSTOKEN\",\n" - " \"Expiration\" : \"3021-09-17T00:41:00Z\"\n}"), + " \"Expiration\" : \"2099-09-17T00:41:00Z\"\n}"), set(PAYLOAD_SIZE, 257) ) ), &provider, &config); + implementation = provider->implementation; + TEST_CHECK(strcmp(implementation->host, "127.0.0.1") == 0); + flb_time_msleep(1000); /* Should successfully get credentials */ @@ -802,8 +882,8 @@ static void test_http_validator_greengrass_invalid_host() struct flb_config *config; /* Test invalid Greengrass URL - correct path but wrong host */ - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://example.com:8080/2016-11-01/credentialprovider/", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://example.com:8080/2016-11-01/credentialprovider/", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); flb_aws_client_mock_configure_generator(NULL); @@ -825,8 +905,8 @@ static void test_http_validator_greengrass_invalid_path() struct flb_config *config; /* Test invalid Greengrass URL - localhost but wrong path */ - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:8080/invalid/path", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:8080/invalid/path", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); flb_aws_client_mock_configure_generator(NULL); @@ -848,8 +928,8 @@ static void test_http_validator_greengrass_invalid_path_prefix() struct flb_config *config; /* Test invalid Greengrass URL - localhost but path has Greengrass pattern in middle, not at start */ - setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:8080/prefix/2016-11-01/credentialprovider/", 1); - setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); + flb_test_setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:8080/prefix/2016-11-01/credentialprovider/", 1); + flb_test_setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "greengrass-token", 1); flb_aws_client_mock_configure_generator(NULL); @@ -865,7 +945,30 @@ static void test_http_validator_greengrass_invalid_path_prefix() flb_free(config); } +static void test_http_mock_optional_values() +{ + struct flb_aws_client_mock_request_chain *chain; + + TEST_CHECK(FLB_AWS_CLIENT_MOCK_COUNT64() == 0); + TEST_CHECK(FLB_AWS_CLIENT_MOCK_COUNT64("one") == 1); + TEST_CHECK(FLB_AWS_CLIENT_MOCK_COUNT64("one", "two") == 2); + + chain = FLB_AWS_CLIENT_MOCK( + response(expect(METHOD, FLB_HTTP_GET), set(STATUS, 200)), + response(expect(HEADER, "Authorization", "test-token")) + ); + + TEST_CHECK(chain->length == 2); + TEST_CHECK(chain->responses[0].length == 2); + TEST_CHECK(chain->responses[1].length == 1); + TEST_CHECK(chain->responses[0].config_parameters[0].config_value_2 == NULL); + TEST_CHECK(chain->responses[0].config_parameters[1].config_value_2 == NULL); + TEST_CHECK(strcmp(chain->responses[1].config_parameters[0].config_value_2, + "test-token") == 0); +} + TEST_LIST = { + { "test_http_mock_optional_values", test_http_mock_optional_values}, { "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}, @@ -875,6 +978,8 @@ TEST_LIST = { { "test_http_provider_https_endpoint", test_http_provider_https_endpoint}, { "test_http_provider_server_failure", test_http_provider_server_failure}, { "test_http_validator_invalid_host", test_http_validator_invalid_host}, + { "test_http_validator_rejects_host_lookalikes", test_http_validator_rejects_host_lookalikes}, + { "test_http_validator_accepts_allowed_addresses", test_http_validator_accepts_allowed_addresses}, { "test_http_validator_invalid_port", test_http_validator_invalid_port}, { "test_http_validator_greengrass_valid", test_http_validator_greengrass_valid}, { "test_http_validator_greengrass_invalid_host", test_http_validator_greengrass_invalid_host},